summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortomsmeding <tom.smeding@gmail.com>2020-01-12 20:04:31 +0100
committertomsmeding <tom.smeding@gmail.com>2020-01-12 20:04:31 +0100
commit5e39d6876b806604090b892369cba9892c7dac25 (patch)
tree7f91fd1d9422fad3ec35f7690755dad9d70a8b9a
parent3a8069d1a46578c810f24c7b1b092251ffc22c39 (diff)
Correctly interpret and change values; does not write yet
-rw-r--r--src/encoding.rs17
-rw-r--r--src/id3v2.rs75
-rw-r--r--src/main.rs59
-rw-r--r--src/options.rs10
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 {