From 38b258bea2c6fc003ecb0e741b71c7b01fdeca77 Mon Sep 17 00:00:00 2001 From: tomsmeding Date: Sun, 19 Jan 2020 20:59:42 +0100 Subject: Add option to remove v1 tags --- src/id3v1.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/id3v2.rs | 2 +- src/main.rs | 50 ++++++++++++++++++++++++++++++++++++++--------- src/options.rs | 2 ++ 4 files changed, 105 insertions(+), 10 deletions(-) create mode 100644 src/id3v1.rs diff --git a/src/id3v1.rs b/src/id3v1.rs new file mode 100644 index 0000000..ccade4a --- /dev/null +++ b/src/id3v1.rs @@ -0,0 +1,61 @@ +use std::fs::File; +use std::io::{self, Read, Seek}; + +#[derive(Copy, Clone, Debug)] +pub enum TagType { + /// An ID3v1.0 tag + TAGv10, + /// An ID3v1.1 enhanced tag (TAG+) in addition to the v1.0 tag + TAGv11, + /// An ID3v1.2 extended tag (EXT) in addition to the v1.0 tag + TAGv12, +} + +pub fn recognise(file: &mut File) -> io::Result> { + let length = file.seek(io::SeekFrom::End(0))?; + if length < 128 { return Ok(None); } + + let mut buf4 = [0u8; 4]; + + // Check whether we have a v1.0 tag + file.seek(io::SeekFrom::End(-128))?; + file.read_exact(&mut buf4[0..3])?; + if &buf4[0..3] == b"TAG" { + // Check whether we also have a v1.1 tag + if length >= 128 + 227 { + file.seek(io::SeekFrom::End(-128 - 227))?; + file.read_exact(&mut buf4)?; + if &buf4 == b"TAG+" { + return Ok(Some(TagType::TAGv11)); + } + } + + // Otherwise, check whether we also have a v1.2 tag + if length >= 128 + 128 { + file.seek(io::SeekFrom::End(-128 - 128))?; + file.read_exact(&mut buf4[0..3])?; + if &buf4[0..3] == b"EXT" { + return Ok(Some(TagType::TAGv12)); + } + } + + // Nope, just v1.0 + Ok(Some(TagType::TAGv10)) + } else { + Ok(None) + } +} + +pub fn remove_tag(file: &mut File, typ: TagType) -> io::Result<()> { + let tag_len = match typ { + TagType::TAGv10 => 128, + TagType::TAGv11 => 128 + 227, + TagType::TAGv12 => 128 + 128, + }; + + let length = file.seek(io::SeekFrom::End(0))?; + eprintln!("remove_tag: Resizing file from {} to {} bytes", length, length - tag_len); + file.set_len(length - tag_len)?; + + Ok(()) +} diff --git a/src/id3v2.rs b/src/id3v2.rs index 19f5e89..b427845 100644 --- a/src/id3v2.rs +++ b/src/id3v2.rs @@ -246,7 +246,7 @@ impl ID3v2 { let mut header = [0u8; 10]; stream.read_exact(&mut header)?; - let (id3version, flags, header_size) = parse_id3v2_header(&header).ok_or("Invalid ID3 header".ioerr())?; + let (id3version, flags, header_size) = parse_id3v2_header(&header).ok_or("Invalid ID3v2 header or no ID3v2 tag found".ioerr())?; let version_sub = match id3version { 0x0300 => 3, diff --git a/src/main.rs b/src/main.rs index 327daa0..5af6eb1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ use crate::options::Options; mod encoding; mod error; +mod id3v1; mod id3v2; mod options; @@ -22,6 +23,10 @@ fn parse_options_into(opt: &mut Options) { .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"); @@ -86,8 +91,36 @@ fn modify_tag(tag: &mut ID3v2, new_frame: Frame) -> io::Result<()> { fn main() -> io::Result<()> { let options = parse_options(); - let mut tag = ID3v2::from_stream(&mut File::open(&options.file)?)?; - // println!("{:?}", tag); + 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 { @@ -99,19 +132,18 @@ fn main() -> io::Result<()> { } } - 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))?; } + 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()?; - let mut f = OpenOptions::new().write(true).open(&options.file)?; - f.write_all(&encoded)?; + open_write()?.write_all(&encoded)?; println!("Tag written!"); } diff --git a/src/options.rs b/src/options.rs index 5f1dec9..b2223de 100644 --- a/src/options.rs +++ b/src/options.rs @@ -1,5 +1,6 @@ pub struct Options { pub latin1_as_utf8: bool, + pub remove_v1: bool, pub file: String, pub write: bool, pub set_tags: TagOptions, @@ -18,6 +19,7 @@ impl Default for Options { fn default() -> Self { Options { latin1_as_utf8: false, + remove_v1: false, file: String::new(), write: false, set_tags: Default::default(), -- cgit v1.2.3