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 id3v1; 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.remove_v1) .add_option(&["--remove-v1"], StoreTrue, "Remove any existing ID3v1.* tags."); 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 open_read = || File::open(&options.file); let open_write = || OpenOptions::new().write(true).open(&options.file); let mut tag = { let (tag, tag_v1) = { let mut file = open_read()?; let tag = ID3v2::from_stream(&mut file)?; let tag_v1 = id3v1::recognise(&mut file)?; (tag, tag_v1) }; if options.remove_v1 { if let Some(tag_type) = tag_v1 { id3v1::remove_tag(&mut open_write()?, tag_type)?; } else { eprintln!("WARNING: Told to remove ID3v1 tag while none is present."); } } else { if let Some(tag_type) = tag_v1 { let descr = match tag_type { id3v1::TagType::TAGv10 => "ID3v1.0", id3v1::TagType::TAGv11 => "ID3v1.1", id3v1::TagType::TAGv12 => "ID3v1.2", }; eprintln!("WARNING: {} tag found at end of file; use --remove-v1 to remove it.", descr); } } 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.clone()))?; } if let Some(s) = &options.set_tags.artist { modify_tag(&mut tag, Frame::TPE1(s.clone()))?; } if let Some(s) = &options.set_tags.title { modify_tag(&mut tag, Frame::TIT2(s.clone()))?; } if let Some(s) = &options.set_tags.track { modify_tag(&mut tag, Frame::TRCK(s.clone()))?; } if let Some(s) = &options.set_tags.year { modify_tag(&mut tag, Frame::TYER(s.clone()))?; } print_tag(&tag)?; // TODO: if -w, then write tags to file (if it fits) if options.write { let encoded = tag.encode()?; open_write()?.write_all(&encoded)?; println!("Tag written!"); } Ok(()) }