use std::io::{Cursor, Read};
use std::fmt::{Display, Formatter, Result as FmtResult};
use crate::ParserError;
#[derive(Debug)]
pub enum Id3 {
V1(Id3V1),
V2(Id3V2),
}
impl Display for Id3 {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
Id3::V1(meta) => write!(f, "{}", meta),
Id3::V2(meta) => write!(f, "{}", meta),
}
}
}
#[derive(Debug, Default)]
pub struct Id3V1 {
pub title: Option<String>,
pub artist: Option<String>,
pub album: Option<String>,
pub year: Option<String>,
pub comment: Option<String>,
pub genre: Option<u8>,
}
impl Display for Id3V1 {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "ID3v1 Metadata:\n")?;
if let Some(title) = &self.title {
write!(f, " Title: {}\n", title)?;
}
if let Some(artist) = &self.artist {
write!(f, " Artist: {}\n", artist)?;
}
if let Some(album) = &self.album {
write!(f, " Album: {}\n", album)?;
}
if let Some(year) = &self.year {
write!(f, " Year: {}\n", year)?;
}
if let Some(comment) = &self.comment {
write!(f, " Comment: {}\n", comment)?;
}
if let Some(genre) = self.genre {
write!(f, " Genre ID: {}\n", genre)?;
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct Id3V2 {
pub title: Option<String>,
pub lead_performer: Option<String>,
pub band_orchestra: Option<String>,
pub subtitle_description_refinement: Option<String>,
pub composer: Option<String>,
pub bpm: Option<String>,
pub content_type: Option<String>,
pub copyright_message: Option<String>,
pub date: Option<String>,
pub publisher: Option<String>,
pub track_number_position_in_set: Option<String>,
pub part_of_set: Option<String>,
pub year: Option<String>,
pub user_defined_text: Option<String>,
pub picture: Option<Vec<u8>>,
pub comments: Vec<String>,
pub unsynchronized_lyric: Option<String>,
pub synchronized_lyric: Option<String>,
pub original_artist: Option<String>,
pub original_album_movie_show_title: Option<String>,
pub original_filename: Option<String>,
pub original_lyricist: Option<String>,
pub file_owner_licensee: Option<String>,
pub internet_radio_station_name: Option<String>,
pub internet_radio_station_owner: Option<String>,
pub official_audio_file_webpage: Option<String>,
pub official_artist_webpage: Option<String>,
pub official_audio_source_webpage: Option<String>,
pub official_internet_radio_station_homepage: Option<String>,
pub payment: Option<String>,
pub publishers_official_webpage: Option<String>,
pub user_defined_url_link: Option<String>,
}
impl Display for Id3V2 {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
if let Some(title) = &self.title {
writeln!(f, "Title: {}", title)?;
}
if let Some(lead) = &self.lead_performer {
writeln!(f, "Lead Performer: {}", lead)?;
}
if let Some(band) = &self.band_orchestra {
writeln!(f, "Band/Orchestra: {}", band)?;
}
if let Some(subtitle) = &self.subtitle_description_refinement {
writeln!(f, "Subtitle/Refinement: {}", subtitle)?;
}
if let Some(composer) = &self.composer {
writeln!(f, "Composer: {}", composer)?;
}
if let Some(bpm) = &self.bpm {
writeln!(f, "BPM: {}", bpm)?;
}
if let Some(content_type) = &self.content_type {
writeln!(f, "Content Type: {}", content_type)?;
}
if let Some(copyright) = &self.copyright_message {
writeln!(f, "Copyright: {}", copyright)?;
}
if let Some(date) = &self.date {
writeln!(f, "Date: {}", date)?;
}
if let Some(publisher) = &self.publisher {
writeln!(f, "Publisher: {}", publisher)?;
}
if let Some(track) = &self.track_number_position_in_set {
writeln!(f, "Track Number/Position: {}", track)?;
}
if let Some(part_of_set) = &self.part_of_set {
writeln!(f, "Part of Set: {}", part_of_set)?;
}
if let Some(year) = &self.year {
writeln!(f, "Year: {}", year)?;
}
if let Some(user_text) = &self.user_defined_text {
writeln!(f, "User Defined Text: {}", user_text)?;
}
if let Some(picture) = &self.picture {
writeln!(f, "Picture: {} bytes", picture.len())?;
}
for comment in &self.comments {
writeln!(f, "Comment: {}", comment)?;
}
Ok(())
}
}
impl Id3 {
pub fn parse(data: &[u8]) -> Result<Self, ParserError> {
if data.starts_with(b"ID3") {
let mut meta = Id3V2::default();
parse_id3v2(data, &mut meta)?;
Ok(Id3::V2(meta))
} else {
let mut meta = Id3V1::default();
parse_id3v1(data, &mut meta);
Ok(Id3::V1(meta))
}
}
}
fn parse_id3v2(data: &[u8], meta: &mut Id3V2) -> Result<usize, ParserError> {
let mut cur = Cursor::new(data);
cur.set_position(3);
let _version_minor = cur.get_ref()[cur.position() as usize];
cur.set_position(cur.position() + 2);
let size_bytes = &data[6..10];
let tag_size = ((size_bytes[0] as u32) << 21)
| ((size_bytes[1] as u32) << 14)
| ((size_bytes[2] as u32) << 7)
| (size_bytes[3] as u32);
let tag_end = 10 + tag_size as usize;
if tag_end > data.len() { return Ok(0); }
let tag_data = &data[10..tag_end];
let mut tag_cur = Cursor::new(tag_data);
while (tag_cur.position() as usize + 10) <= tag_data.len() {
let mut id_bytes = [0u8; 4];
if tag_cur.read_exact(&mut id_bytes).is_err() { break; }
let mut size_bytes = [0u8; 4];
if tag_cur.read_exact(&mut size_bytes).is_err() { break; }
let size = u32::from_be_bytes(size_bytes) as usize;
tag_cur.set_position(tag_cur.position() + 2);
if size == 0 || (tag_cur.position() as usize + size) > tag_data.len() { break; }
let mut content = vec![0u8; size];
if tag_cur.read_exact(&mut content).is_err() { break; }
let text = decode_text(&content[1..]);
match &id_bytes {
b"TIT2" => meta.title = text,
b"TPE1" => meta.lead_performer = text,
b"TPE2" => meta.band_orchestra = text,
b"TIT3" => meta.subtitle_description_refinement = text,
b"TCOM" => meta.composer = text,
b"TBPM" => meta.bpm = text,
b"TCON" => meta.content_type = text,
b"TCOP" => meta.copyright_message = text,
b"TDAT" => meta.date = text,
b"TPUB" => meta.publisher = text,
b"TRCK" => meta.track_number_position_in_set = text,
b"TPOS" => meta.part_of_set = text,
b"TYER" => meta.year = text,
b"TXXX" => meta.user_defined_text = text,
b"COMM" => { if let Some(t) = text { meta.comments.push(t); } }
_ => {}
}
}
Ok(tag_end)
}
fn parse_id3v1(data: &[u8], meta: &mut Id3V1) {
if data.len() < 128 { return; }
let tag = &data[data.len() - 128..];
if &tag[0..3] != b"TAG" { return; }
let read_str = |slice: &[u8]| {
let s = String::from_utf8_lossy(slice)
.trim_matches('\0')
.trim()
.to_string();
if s.is_empty() { None } else { Some(s) }
};
meta.title = read_str(&tag[3..33]);
meta.artist = read_str(&tag[33..63]);
meta.album = read_str(&tag[63..93]);
meta.year = read_str(&tag[93..97]);
meta.comment = read_str(&tag[97..127]);
meta.genre = Some(tag[127]);
}
fn decode_text(data: &[u8]) -> Option<String> {
if data.is_empty() { return None; }
Some(String::from_utf8_lossy(data).trim_matches('\0').to_string())
}