summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/encoding.rs14
-rw-r--r--src/error.rs15
-rw-r--r--src/id3v2.rs95
-rw-r--r--src/main.rs86
-rw-r--r--src/options.rs4
5 files changed, 132 insertions, 82 deletions
diff --git a/src/encoding.rs b/src/encoding.rs
index a3851aa..c117cd0 100644
--- a/src/encoding.rs
+++ b/src/encoding.rs
@@ -42,10 +42,10 @@ 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) -> 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,
+ }
+}
diff --git a/src/error.rs b/src/error.rs
index 6358bfe..19bd48d 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -4,19 +4,8 @@ pub trait IntoIOError {
fn ioerr(self) -> io::Error;
}
-impl IntoIOError for String {
- fn ioerr(self) -> io::Error {
- io::Error::new(io::ErrorKind::Other, self)
- }
-}
-
-impl IntoIOError for &str {
- fn ioerr(self) -> io::Error {
- io::Error::new(io::ErrorKind::Other, self)
- }
-}
-
-impl IntoIOError for std::string::FromUtf8Error {
+// This impl bound is taken directly from the io::Error::new function.
+impl<E: Into<Box<dyn std::error::Error + Send + Sync>>> IntoIOError for E {
fn ioerr(self) -> io::Error {
io::Error::new(io::ErrorKind::Other, self)
}
diff --git a/src/id3v2.rs b/src/id3v2.rs
index a8dccb9..934bdf3 100644
--- a/src/id3v2.rs
+++ b/src/id3v2.rs
@@ -1,9 +1,11 @@
// http://id3.org/id3v2.3.0
+use std::convert::TryFrom;
use std::io::{self, Read};
+use std::iter;
+use std::num::TryFromIntError;
use crate::encoding::{from_latin_1, from_ucs_2_bom};
use crate::error::IntoIOError;
-use crate::options::EncodingOptions;
use crate::util::read_big_endian;
fn parse_id3v2_header(bytes: &[u8]) -> Option<(u16, u8, usize)> {
@@ -46,6 +48,30 @@ pub enum Frame {
TRCK(String),
}
+struct ArrayIter2<T> {
+ arr: [T; 2],
+ cursor: u8,
+}
+
+impl<T> ArrayIter2<T> {
+ fn new(arr: [T; 2]) -> Self {
+ Self { arr, cursor: 0 }
+ }
+}
+
+impl<T: Clone> Iterator for ArrayIter2<T> {
+ type Item = T;
+ fn next(&mut self) -> Option<T> {
+ if (self.cursor as usize) < self.arr.len() {
+ let i = self.cursor;
+ self.cursor += 1;
+ Some(self.arr[i as usize].clone())
+ } else {
+ None
+ }
+ }
+}
+
impl RawFrame {
fn parse(data: &[u8]) -> Result<Option<(Self, usize)>, String> {
if data.len() < 10 {
@@ -69,16 +95,12 @@ impl RawFrame {
Ok(Some((RawFrame { id, flags, body }, 10 + size)))
}
- fn interpret_encoded_string(&self, encopts: &EncodingOptions) -> io::Result<String> {
+ fn interpret_encoded_string(&self) -> io::Result<String> {
match self.body.get(0).ok_or("String field too small".ioerr())? {
0 => { // Latin-1
let mut i = self.body.len();
while i > 0 && self.body[i-1] == 0 { i -= 1; }
- if encopts.latin1_as_utf8 {
- String::from_utf8(self.body[1..i].to_vec()).map_err(|e| e.ioerr())
- } else {
- from_latin_1(&self.body[1..i]).ok_or("Invalid Latin-1 string field".ioerr())
- }
+ from_latin_1(&self.body[1..i]).ok_or("Invalid Latin-1 string field".ioerr())
}
1 => { // UCS-2
@@ -93,21 +115,56 @@ impl RawFrame {
}
}
- pub fn interpret(&self, encopts: &EncodingOptions) -> io::Result<Option<Frame>> {
- if self.id == "TIT2" {
- self.interpret_encoded_string(encopts).map(Frame::TIT2).map(Some)
- } else if self.id == "TYER" {
- self.interpret_encoded_string(encopts).map(Frame::TYER).map(Some)
- } else if self.id == "TPE1" {
- self.interpret_encoded_string(encopts).map(Frame::TPE1).map(Some)
- } else if self.id == "TALB" {
- self.interpret_encoded_string(encopts).map(Frame::TALB).map(Some)
- } else if self.id == "TRCK" {
- self.interpret_encoded_string(encopts).map(Frame::TRCK).map(Some)
- } else {
+ 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);
+
+ if self.id == "TIT2" { type_t(Frame::TIT2) }
+ else if self.id == "TYER" { type_t(Frame::TYER) }
+ else if self.id == "TPE1" { type_t(Frame::TPE1) }
+ else if self.id == "TALB" { type_t(Frame::TALB) }
+ else if self.id == "TRCK" { type_t(Frame::TRCK) }
+ else {
Ok(None)
}
}
+
+ pub fn map_string<F: Fn(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)?,
+ })
+ };
+
+ match self.interpret()? {
+ Some(Frame::TIT2(s)) => Ok(Some(type_t("TIT2", f(s))?)),
+ Some(Frame::TYER(s)) => Ok(Some(type_t("TYER", f(s))?)),
+ 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)),
+ }
+ }
}
impl ID3v2 {
diff --git a/src/main.rs b/src/main.rs
index 3939e17..63bbbf0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,8 @@
use std::io;
use std::fs::File;
use argparse::{ArgumentParser, StoreTrue, Store};
-use crate::options::{EncodingOptions, Options};
+use crate::options::Options;
+use crate::id3v2::{ID3v2, Frame};
mod encoding;
mod error;
@@ -9,57 +10,64 @@ mod id3v2;
mod options;
mod util;
-fn main() -> io::Result<()> {
- let options = {
- let mut options: Options = Default::default();
+fn parse_options_into(opt: &mut Options) {
+ let mut ap = ArgumentParser::new();
+ ap.set_description("ID3v2 tag editor/fixer. Incomplete/work-in-progress.");
+
+ 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.");
- {
- let mut ap = ArgumentParser::new();
- ap.set_description("ID3v2 tag editor/fixer. Incomplete/work-in-progress.");
+ 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 options.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.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 options.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.file)
+ .required()
+ .add_argument("file", Store,
+ "File to operate on (probably a .mp3)");
- ap.refer(&mut options.set_tags.album) .add_option(&["-A", "--album"], Store, "Set/replace album");
- ap.refer(&mut options.set_tags.artist).add_option(&["-a", "--artist"], Store, "Set/replace artist");
- ap.refer(&mut options.set_tags.title) .add_option(&["-t", "--title"], Store, "Set/replace title");
- ap.refer(&mut options.set_tags.track) .add_option(&["-T", "--track"], Store, "Set/replace track number (num or num/num)");
- ap.refer(&mut options.set_tags.year) .add_option(&["-y", "--year"], Store, "Set/replace year");
+ ap.parse_args_or_exit();
+}
- ap.refer(&mut options.file)
- .required()
- .add_argument("file", Store,
- "File to operate on (probably a .mp3)");
+fn parse_options() -> Options {
+ let mut options = Default::default();
+ parse_options_into(&mut options);
+ options
+}
- ap.parse_args_or_exit();
+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(())
+}
- options
- };
+fn main() -> io::Result<()> {
+ let options = parse_options();
- let mut f = File::open(options.file)?;
- let tag = id3v2::ID3v2::from_stream(&mut f)?;
+ let tag = ID3v2::from_stream(&mut File::open(options.file)?)?;
// println!("{:?}", tag);
// TODO: apply changes from options here
+ // Use RawFrame::map_string
- for frame in tag.frames {
- match frame.interpret(&EncodingOptions { latin1_as_utf8: options.latin1_as_utf8 })? {
- Some(id3v2::Frame::TALB(album)) => println!("Album : '{}'", album),
- Some(id3v2::Frame::TIT2(title)) => println!("Title : '{}'", title),
- Some(id3v2::Frame::TPE1(artist)) => println!("Artist: '{}'", artist),
- Some(id3v2::Frame::TYER(year)) => println!("Year : '{}'", year),
- Some(id3v2::Frame::TRCK(track)) => println!("Track : '{}'", track),
- None => {
- println!("Unknown frame: {:?}", frame);
- }
- }
- }
+ print_tag(&tag)?;
// TODO: if -w, then write tags to file (if it fits)
diff --git a/src/options.rs b/src/options.rs
index e51004a..ceba05d 100644
--- a/src/options.rs
+++ b/src/options.rs
@@ -14,10 +14,6 @@ pub struct TagOptions {
pub year: String,
}
-pub struct EncodingOptions {
- pub latin1_as_utf8: bool,
-}
-
impl Default for Options {
fn default() -> Self {
Options {