diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/encoding.rs | 17 | ||||
-rw-r--r-- | src/id3v2.rs | 75 | ||||
-rw-r--r-- | src/main.rs | 59 | ||||
-rw-r--r-- | src/options.rs | 10 |
4 files changed, 118 insertions, 43 deletions
diff --git a/src/encoding.rs b/src/encoding.rs index c117cd0..fc0c335 100644 --- a/src/encoding.rs +++ b/src/encoding.rs @@ -1,4 +1,6 @@ use std::convert::TryFrom; +use std::io; +use crate::error::IntoIOError; use crate::util::read_big_endian; macro_rules! guard { @@ -42,10 +44,13 @@ pub fn from_ucs_2_bom(bytes: &[u8]) -> Option<String> { Some(res) } -pub fn from_utf8_mistaken_as_latin1(latin1: &str) -> Option<String> { - guard!(latin1.chars().all(|c| (c as usize) < 256)); - match std::str::from_utf8(latin1.as_bytes()) { - Ok(res) => Some(res.to_string()), - Err(_) => None, - } +pub fn from_utf8_mistaken_as_latin1(latin1: &str) -> io::Result<String> { + latin1 + .chars() + .map(|c| u8::try_from(u32::from(c))) + .collect::<Result<Vec<u8>, _>>() + .map_err(|e| e.ioerr()) + .and_then(|v| std::str::from_utf8(&v) + .map(|s| s.to_string()) + .map_err(|e| e.ioerr())) } diff --git a/src/id3v2.rs b/src/id3v2.rs index 934bdf3..9e07cd3 100644 --- a/src/id3v2.rs +++ b/src/id3v2.rs @@ -26,6 +26,28 @@ fn parse_id3v2_header(bytes: &[u8]) -> Option<(u16, u8, usize)> { } } +/// Encodes native string to ID3v2 string encoding (either Latin-1 or UCS-2 according to the +/// characters used). +fn encode_string(s: &str) -> io::Result<Vec<u8>> { + let as_latin1 = || iter::once(Ok(0)) + .chain(s.chars().map(|c| u8::try_from(u32::from(c)))) + .collect::<Result<Vec<u8>, _>>(); + + let as_ucs2 = || { + let nibbles = s.chars() + .map(|c| u16::try_from(u32::from(c))) + .collect::<Result<Vec<u16>, _>>()?; + Ok(iter::once(1) + .chain(ArrayIter2::new([0xfe, 0xff])) // BOM + .chain(nibbles.iter().flat_map(|&n| ArrayIter2::new([(n >> 8) as u8, n as u8]))) + .collect::<Vec<u8>>()) + }; + + as_latin1() + .or_else(|_| as_ucs2()) + .map_err(|e: TryFromIntError| e.ioerr()) +} + #[derive(Debug)] pub struct ID3v2 { header_size: usize, @@ -115,25 +137,6 @@ impl RawFrame { } } - fn encode_string(s: &str) -> io::Result<Vec<u8>> { - let as_latin1 = || iter::once(Ok(0)) - .chain(s.chars().map(|c| u8::try_from(u32::from(c)))) - .collect::<Result<Vec<u8>, _>>(); - - let as_ucs2 = || { - let nibbles = s.chars() - .map(|c| u16::try_from(u32::from(c))) - .collect::<Result<Vec<u16>, _>>()?; - Ok(iter::once(1) - .chain(nibbles.iter().flat_map(|&n| ArrayIter2::new([(n >> 8) as u8, n as u8]))) - .collect::<Vec<u8>>()) - }; - - as_latin1() - .or_else(|_| as_ucs2()) - .map_err(|e: TryFromIntError| e.ioerr()) - } - pub fn interpret(&self) -> io::Result<Option<Frame>> { let type_t = |typ: fn(String) -> Frame| self.interpret_encoded_string().map(typ).map(Some); @@ -147,12 +150,12 @@ impl RawFrame { } } - pub fn map_string<F: Fn(String) -> String>(self, f: F) -> io::Result<Option<Self>> { + pub fn map_string<F: FnOnce(String) -> String>(&self, f: F) -> io::Result<Option<Self>> { let type_t = |id: &str, body: String| -> io::Result<Self> { Ok(Self { id: id.to_string(), flags: 0, - body: Self::encode_string(&body)?, + body: encode_string(&body)?, }) }; @@ -162,7 +165,35 @@ impl RawFrame { Some(Frame::TPE1(s)) => Ok(Some(type_t("TPE1", f(s))?)), Some(Frame::TALB(s)) => Ok(Some(type_t("TALB", f(s))?)), Some(Frame::TRCK(s)) => Ok(Some(type_t("TRCK", f(s))?)), - None => Ok(Some(self)), + None => Ok(None), + } + } + + pub fn get_id(&self) -> &str { + &self.id + } +} + +impl Frame { + pub fn to_raw(self) -> io::Result<RawFrame> { + let type_t = |typ: &str, body: String| Ok(RawFrame { id: typ.to_string(), flags: 0, body: encode_string(&body)? }); + + match self { + Self::TIT2(s) => type_t("TIT2", s), + Self::TYER(s) => type_t("TYER", s), + Self::TPE1(s) => type_t("TPE1", s), + Self::TALB(s) => type_t("TALB", s), + Self::TRCK(s) => type_t("TRCK", s), + } + } + + pub fn id(&self) -> &str { + match self { + Frame::TIT2(_) => "TIT2", + Frame::TYER(_) => "TYER", + Frame::TPE1(_) => "TPE1", + Frame::TALB(_) => "TALB", + Frame::TRCK(_) => "TRCK", } } } diff --git a/src/main.rs b/src/main.rs index 63bbbf0..c2b4c5a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use std::io; use std::fs::File; -use argparse::{ArgumentParser, StoreTrue, Store}; -use crate::options::Options; +use argparse::{ArgumentParser, Store, StoreTrue, StoreOption}; +use crate::error::IntoIOError; use crate::id3v2::{ID3v2, Frame}; +use crate::options::Options; mod encoding; mod error; @@ -22,11 +23,11 @@ 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.set_tags.album) .add_option(&["-A", "--album"], Store, "Set/replace album"); - ap.refer(&mut opt.set_tags.artist).add_option(&["-a", "--artist"], Store, "Set/replace artist"); - ap.refer(&mut opt.set_tags.title) .add_option(&["-t", "--title"], Store, "Set/replace title"); - ap.refer(&mut opt.set_tags.track) .add_option(&["-T", "--track"], Store, "Set/replace track number (num or num/num)"); - ap.refer(&mut opt.set_tags.year) .add_option(&["-y", "--year"], Store, "Set/replace year"); + 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() @@ -58,14 +59,52 @@ fn print_tag(tag: &ID3v2) -> io::Result<()> { 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(new_frame.to_raw()?); + } + + 1 => { + tag.frames[indices[0]] = new_frame.to_raw()?; + } + + _ => { + return Err(format!("Multiple frames of type {} found", new_frame.id()).ioerr()); + } + } + + Ok(()) +} + fn main() -> io::Result<()> { let options = parse_options(); - let tag = ID3v2::from_stream(&mut File::open(options.file)?)?; + let mut tag = ID3v2::from_stream(&mut File::open(options.file)?)?; // println!("{:?}", tag); - // TODO: apply changes from options here - // Use RawFrame::map_string + 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)?; diff --git a/src/options.rs b/src/options.rs index ceba05d..5f1dec9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -7,11 +7,11 @@ pub struct Options { #[derive(Default)] pub struct TagOptions { - pub album: String, - pub artist: String, - pub title: String, - pub track: String, - pub year: String, + pub album: Option<String>, + pub artist: Option<String>, + pub title: Option<String>, + pub track: Option<String>, + pub year: Option<String>, } impl Default for Options { |