use std::io::{self, Write}; use std::fs::{File, OpenOptions}; use argparse::{ArgumentParser, Store, StoreTrue, StoreOption}; use crate::error::IntoIOError; use crate::id3v2::{ID3v2, Frame}; use crate::options::Options; mod encoding; mod error; mod id3v2; mod options; fn parse_options_into(opt: &mut Options) { let mut ap = ArgumentParser::new(); ap.set_description("ID3v2 tag editor/fixer. Incomplete/work-in-progress. Support for ID3v2.3, with partial support of ID3v2.4 (no footer tags supported)."); ap.refer(&mut opt.write) .add_option(&["-w", "--write"], StoreTrue, "Write updated information instead of just displaying. Unmodified or unknown tags are preserved as-is."); ap.refer(&mut opt.latin1_as_utf8) .add_option(&["--assume-utf8"], StoreTrue, "Assume that all strings specified in the MP3 as Latin-1 are really UTF-8."); ap.refer(&mut opt.set_tags.album) .add_option(&["-A", "--album"], StoreOption, "Set/replace album"); ap.refer(&mut opt.set_tags.artist).add_option(&["-a", "--artist"], StoreOption, "Set/replace artist"); ap.refer(&mut opt.set_tags.title) .add_option(&["-t", "--title"], StoreOption, "Set/replace title"); ap.refer(&mut opt.set_tags.track) .add_option(&["-T", "--track"], StoreOption, "Set/replace track number (num or num/num)"); ap.refer(&mut opt.set_tags.year) .add_option(&["-y", "--year"], StoreOption, "Set/replace year"); ap.refer(&mut opt.file) .required() .add_argument("file", Store, "File to operate on (probably a .mp3)"); ap.parse_args_or_exit(); } fn parse_options() -> Options { let mut options = Default::default(); parse_options_into(&mut options); options } fn print_tag(tag: &ID3v2) -> io::Result<()> { for frame in &tag.frames { match frame.interpret()? { Some(Frame::TALB(album)) => println!("Album : '{}'", album), Some(Frame::TIT2(title)) => println!("Title : '{}'", title), Some(Frame::TPE1(artist)) => println!("Artist: '{}'", artist), Some(Frame::TYER(year)) => println!("Year : '{}'", year), Some(Frame::TRCK(track)) => println!("Track : '{}'", track), None => { println!("Unknown frame: {:?}", frame); } } } Ok(()) } fn modify_tag(tag: &mut ID3v2, new_frame: Frame) -> io::Result<()> { let mut indices = Vec::new(); for (i, frame) in tag.frames.iter().enumerate() { if frame.get_id() == new_frame.id() { indices.push(i); } } match indices.len() { 0 => { tag.frames.push(tag.to_raw(new_frame)?); } 1 => { tag.frames[indices[0]] = tag.to_raw(new_frame)?; } _ => { return Err(format!("Multiple frames of type {} found", new_frame.id()).ioerr()); } } Ok(()) } fn main() -> io::Result<()> { let options = parse_options(); let mut tag = ID3v2::from_stream(&mut File::open(&options.file)?)?; // println!("{:?}", tag); if options.latin1_as_utf8 { for frame in &mut tag.frames { if let Some(new_frame) = frame.map_string(|s| encoding::from_utf8_mistaken_as_latin1(&s) .expect("String cannot be encoded in ID3v2 (outside of Unicode BMP)"))? { *frame = new_frame; } } } if let Some(s) = options.set_tags.album { modify_tag(&mut tag, Frame::TALB(s))?; } if let Some(s) = options.set_tags.artist { modify_tag(&mut tag, Frame::TPE1(s))?; } if let Some(s) = options.set_tags.title { modify_tag(&mut tag, Frame::TIT2(s))?; } if let Some(s) = options.set_tags.track { modify_tag(&mut tag, Frame::TRCK(s))?; } if let Some(s) = options.set_tags.year { modify_tag(&mut tag, Frame::TYER(s))?; } print_tag(&tag)?; // TODO: if -w, then write tags to file (if it fits) if options.write { let encoded = tag.encode()?; let mut f = OpenOptions::new().write(true).open(&options.file)?; f.write_all(&encoded)?; } Ok(()) }