This commit is contained in:
Tianyi 2020-10-27 11:40:51 +00:00
parent d2d1bedafa
commit 6abb1e9d4d
12 changed files with 272 additions and 226 deletions

View file

@ -1,3 +1,7 @@
## [0.2.5] 2020-10-27
- Naive implementation of config
## [0.2.3] 2020-10-27
- multiple artists

View file

@ -1,6 +1,6 @@
[package]
name = "audiotags"
version = "0.2.3"
version = "0.2.5"
authors = ["Tianyi <ShiTianyi2001@outlook.com>"]
edition = "2018"
description = "Unified IO for different types of audio metadata"
@ -15,3 +15,4 @@ mp4ameta = "0.6"
metaflac = "0.2"
beef = "0.4.4"
thiserror = "1.0.21"
audiotags-dev-macro = {path = "./audiotags-dev-macro"}

View file

@ -100,8 +100,10 @@ fn main() {
Some(vec!["artist1 of mp4", "artist2 of mp4"])
);
// convert to id3 tag, which does not support multiple artists
let mp3tag = mp4tag.into_tag(TagType::Id3v2);
assert_eq!(mp3tag.artist(), Some("artist1 of mp4;artist2 of mp4"));
let mp3tag = mp4tag
.with_config(Config::default().sep_artist("/")) // separator is by default `;`
.into_tag(TagType::Id3v2);
assert_eq!(mp3tag.artist(), Some("artist1 of mp4/artist2 of mp4"));
}
```
@ -116,7 +118,7 @@ fn main() {
## Getters and Setters
```rust
pub trait AudioTagIo {
pub trait AudioTag {
fn title(&self) -> Option<&str>;
fn set_title(&mut self, title: &str);
fn remove_title(&mut self);

11
audiotags-dev-macro/.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
# Generated by Cargo
# will have compiled files and executables
/target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk

View file

@ -0,0 +1,9 @@
[package]
name = "audiotags-dev-macro"
version = "0.1.0"
authors = ["Tianyi <ShiTianyi2001@outlook.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

View file

@ -0,0 +1,35 @@
#[macro_export]
macro_rules! impl_tag {
($tag:ident , $inner:ident) => {
#[derive(Default)]
pub struct $tag {
inner: $inner,
config: Config,
}
impl $tag {
pub fn new() -> Self {
Self::default()
}
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
Ok(Self {
inner: $inner::read_from_path(path)?,
config: Config::default(),
})
}
}
impl AudioTagCommon for $tag {
fn config(&self) -> &Config {
&self.config
}
fn with_config(&self, config: Config) -> Box<dyn AudioTag> {
Box::new(Self {
inner: self.inner.clone(),
config,
})
}
fn into_anytag(&self) -> AnyTag<'_> {
self.into()
}
}
};
}

27
src/config.rs Normal file
View file

@ -0,0 +1,27 @@
#[derive(Clone, Copy)]
pub struct Config {
/// The separator used when parsing and formatting multiple artists in metadata formats that does not explicitly support
/// multiple artists (i.e. artist is a single string separated by the separator)
pub sep_artist: &'static str,
/// Parse multiple artists from a single string using the separator specified above
pub parse_multiple_artists: bool,
}
impl<'a> Default for Config {
fn default() -> Self {
Self {
sep_artist: ";",
parse_multiple_artists: true,
}
}
}
impl Config {
pub fn sep_artist(mut self, sep: &'static str) -> Self {
self.sep_artist = sep;
self
}
pub fn parse_multiple_artists(mut self, parse_multiple_artists: bool) -> Self {
self.parse_multiple_artists = parse_multiple_artists;
self
}
}

View file

@ -1,9 +1,8 @@
use super::*;
use metaflac;
use metaflac::Tag as InnerTag;
pub struct FlacTag {
inner: metaflac::Tag,
}
impl_tag!(FlacTag, InnerTag);
impl<'a> From<AnyTag<'a>> for FlacTag {
fn from(inp: AnyTag<'a>) -> Self {
@ -13,7 +12,7 @@ impl<'a> From<AnyTag<'a>> for FlacTag {
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(SEP_ARTIST);
v.push_str(inp.config.sep_artist);
v
})
})
@ -24,7 +23,7 @@ impl<'a> From<AnyTag<'a>> for FlacTag {
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(SEP_ARTIST);
v.push_str(inp.config.sep_artist);
v
})
})
@ -41,10 +40,14 @@ impl<'a> From<&'a FlacTag> for AnyTag<'a> {
fn from(inp: &'a FlacTag) -> Self {
let mut t = Self::default();
t.title = inp.title().map(Cow::borrowed);
t.artists = inp.artist().map(|v| vec![Cow::borrowed(v)]);
t.artists = inp
.artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>());
t.year = inp.year();
t.album_title = inp.album_title().map(Cow::borrowed);
t.album_artists = inp.album_artist().map(|v| vec![Cow::borrowed(v)]);
t.album_artists = inp
.album_artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>());
t.album_cover = inp.album_cover();
t.track_number = inp.track_number();
t.total_tracks = inp.total_tracks();
@ -54,20 +57,7 @@ impl<'a> From<&'a FlacTag> for AnyTag<'a> {
}
}
impl Default for FlacTag {
fn default() -> Self {
Self {
inner: metaflac::Tag::default(),
}
}
}
impl FlacTag {
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
Ok(Self {
inner: metaflac::Tag::read_from_path(path)?,
})
}
pub fn get_first(&self, key: &str) -> Option<&str> {
if let Some(Some(v)) = self.inner.vorbis_comments().map(|c| c.get(key)) {
if !v.is_empty() {
@ -87,10 +77,7 @@ impl FlacTag {
}
}
impl AudioTagIo for FlacTag {
fn into_anytag(&self) -> AnyTag<'_> {
self.into()
}
impl AudioTag for FlacTag {
fn title(&self) -> Option<&str> {
self.get_first("TITLE")
}

View file

@ -1,94 +1,67 @@
use super::*;
use id3;
pub struct Id3v2Tag {
inner: id3::Tag,
}
use id3::Tag as InnerTag;
impl Default for Id3v2Tag {
fn default() -> Self {
Self {
inner: id3::Tag::default(),
}
}
}
impl Id3v2Tag {
pub fn new() -> Self {
Self {
inner: id3::Tag::default(),
}
}
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
Ok(Self {
inner: id3::Tag::read_from_path(path)?,
})
}
}
impl_tag!(Id3v2Tag, InnerTag);
impl<'a> From<&'a Id3v2Tag> for AnyTag<'a> {
fn from(inp: &'a Id3v2Tag) -> Self {
(&inp.inner).into()
Self {
config: inp.config.clone(),
title: inp.title().map(Cow::borrowed),
artists: inp
.artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>()),
year: inp.year(),
album_title: inp.album_title().map(Cow::borrowed),
album_artists: inp
.album_artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>()),
album_cover: inp.album_cover(),
track_number: inp.track_number(),
total_tracks: inp.total_tracks(),
disc_number: inp.disc_number(),
total_discs: inp.total_discs(),
}
}
}
impl<'a> From<AnyTag<'a>> for Id3v2Tag {
fn from(inp: AnyTag<'a>) -> Self {
Self { inner: inp.into() }
}
}
impl<'a> From<&'a id3::Tag> for AnyTag<'a> {
fn from(inp: &'a id3::Tag) -> Self {
let u32tou16 = |x: u32| x as u16;
let mut t = Self::default();
t.title = inp.title().map(Cow::borrowed);
t.artists = inp.artist().map(|v| vec![Cow::borrowed(v)]);
t.year = inp.year();
t.album_title = inp.album().map(Cow::borrowed);
t.album_artists = inp.album_artist().map(|v| vec![Cow::borrowed(v)]);
t.album_cover = inp
.pictures()
.filter(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverFront))
.next()
.and_then(|pic| Picture::try_from(pic).ok());
t.track_number = inp.track().map(u32tou16);
t.total_tracks = inp.total_tracks().map(u32tou16);
t.disc_number = inp.disc().map(u32tou16);
t.total_discs = inp.total_discs().map(u32tou16);
t
}
}
impl<'a> From<AnyTag<'a>> for id3::Tag {
fn from(inp: AnyTag<'a>) -> Self {
let mut t = id3::Tag::new();
inp.title().map(|v| t.set_title(v));
inp.artists()
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(SEP_ARTIST);
v
})
})
.map(|v| t.set_artist(&v[..v.len() - 1]));
inp.year.map(|v| t.set_year(v));
inp.album_title().map(|v| t.set_album(v));
inp.album_artists()
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(SEP_ARTIST);
v
})
})
.map(|v| t.set_album_artist(&v[..v.len() - 1]));
inp.track_number().map(|v| t.set_track(v as u32));
inp.total_tracks().map(|v| t.set_total_tracks(v as u32));
inp.disc_number().map(|v| t.set_disc(v as u32));
inp.total_discs().map(|v| t.set_total_discs(v as u32));
t
Self {
config: inp.config.clone(),
inner: {
let mut t = id3::Tag::new();
inp.title().map(|v| t.set_title(v));
inp.artists()
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(inp.config.sep_artist);
v
})
})
.map(|v| t.set_artist(&v[..v.len() - 1]));
inp.year.map(|v| t.set_year(v));
inp.album_title().map(|v| t.set_album(v));
inp.album_artists()
.map(|i| {
i.iter().fold(String::new(), |mut v, a| {
v.push_str(&a);
v.push_str(inp.config.sep_artist);
v
})
})
.map(|v| t.set_album_artist(&v[..v.len() - 1]));
inp.track_number().map(|v| t.set_track(v as u32));
inp.total_tracks().map(|v| t.set_total_tracks(v as u32));
inp.disc_number().map(|v| t.set_disc(v as u32));
inp.total_discs().map(|v| t.set_total_discs(v as u32));
t
},
}
}
}
@ -122,7 +95,7 @@ impl<'a> std::convert::TryFrom<id3::frame::Picture> for Picture<'a> {
}
}
impl AudioTagIo for Id3v2Tag {
impl AudioTag for Id3v2Tag {
fn title(&self) -> Option<&str> {
self.inner.title()
}
@ -248,10 +221,6 @@ impl AudioTagIo for Id3v2Tag {
self.inner.write_to_path(path, id3::Version::Id3v24)?;
Ok(())
}
fn into_anytag(&self) -> AnyTag<'_> {
self.into()
}
}
// impl<'a> From<AnyTag<'a>> for Id3Tag {

View file

@ -107,7 +107,7 @@
//! ## Getters and Setters
//!
//! ```ignore
//! pub trait AudioTagIo {
//! pub trait AudioTag {
//! fn title(&self) -> Option<&str>;
//! fn set_title(&mut self, title: &str);
//! fn remove_title(&mut self);
@ -153,6 +153,8 @@
//! }
//! ```
pub(crate) use audiotags_dev_macro::*;
mod id3_tag;
pub use id3_tag::Id3v2Tag;
mod flac_tag;
@ -163,6 +165,9 @@ pub use mp4_tag::Mp4Tag;
pub mod error;
pub use error::{Error, Result};
pub mod config;
pub use config::Config;
use std::convert::From;
use std::fs::File;
use std::path::Path;
@ -209,16 +214,30 @@ impl TagType {
#[derive(Default)]
pub struct Tag {
tag_type: Option<TagType>,
config: Config,
}
impl Tag {
pub fn with_tag_type(tag_type: TagType) -> Self {
Self {
tag_type: Some(tag_type),
config: Config::default(),
}
}
// pub fn with_config(config: Config) -> Self {
// Self {
// tag_type: None,
// config: config.clone(),
// }
// }
// pub fn with_tag_type_and_config(tag_type: TagType, config: Config) -> Self {
// Self {
// tag_type: Some(tag_type),
// config: config.clone(),
// }
// }
pub fn read_from_path(&self, path: impl AsRef<Path>) -> crate::Result<Box<dyn AudioTagIo>> {
pub fn read_from_path(&self, path: impl AsRef<Path>) -> crate::Result<Box<dyn AudioTag>> {
match self.tag_type.unwrap_or(TagType::try_from_ext(
path.as_ref()
.extension()
@ -228,9 +247,15 @@ impl Tag {
.to_lowercase()
.as_str(),
)?) {
TagType::Id3v2 => Ok(Box::new(Id3v2Tag::read_from_path(path)?)),
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path)?)),
TagType::Flac => Ok(Box::new(FlacTag::read_from_path(path)?)),
TagType::Id3v2 => Ok(Box::new(
Id3v2Tag::read_from_path(path)?, //).with_config(self.config.clone()),
)),
TagType::Mp4 => Ok(Box::new(
Mp4Tag::read_from_path(path)?, //).with_config(self.config.clone()),
)),
TagType::Flac => Ok(Box::new(
FlacTag::read_from_path(path)?, //.with_config(self.config.clone()),
)),
}
}
}
@ -328,10 +353,9 @@ impl<'a> Album<'a> {
}
}
const SEP_ARTIST: &'static str = ";";
#[derive(Default)]
pub struct AnyTag<'a> {
pub config: Config,
pub title: Option<Cow<'a, str>>,
pub artists: Option<Vec<Cow<'a, str>>>, // ? iterator
pub year: Option<i32>,
@ -344,7 +368,7 @@ pub struct AnyTag<'a> {
pub total_discs: Option<u16>,
}
impl<'a> AnyTag<'a> {
impl AnyTag<'_> {
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
@ -374,24 +398,26 @@ impl<'a> AnyTag<'a> {
}
}
impl AnyTag<'_> {
pub fn artists_as_string(&self) -> Option<String> {
self.artists()
.map(|artists| artists.join(self.config.sep_artist))
}
pub fn album_artists_as_string(&self) -> Option<String> {
self.album_artists()
.map(|artists| artists.join(self.config.sep_artist))
}
}
pub trait TagIo {
fn read_from_path(path: &str) -> crate::Result<AnyTag>;
fn write_to_path(path: &str) -> crate::Result<()>;
}
// impl<'a> AnyTag<'a> {
// fn read_from_path(path: &str, tag_type: TagType) -> StdResult<Self, BoxedError> {
// match tag_type {
// TagType::Id3v2 => Ok(Id3v2Tag::read_from_path(path)?.into()),
// _ => Err(Box::new(Error::UnsupportedFormat(".".to_owned()))),
// }
// }
// }
/// Implementors of this trait are able to read and write audio metadata.
///
/// Constructor methods e.g. `from_file` should be implemented separately.
pub trait AudioTagIo {
pub trait AudioTag: AudioTagCommon {
fn title(&self) -> Option<&str>;
fn set_title(&mut self, title: &str);
fn remove_title(&mut self);
@ -401,7 +427,12 @@ pub trait AudioTagIo {
fn remove_artist(&mut self);
fn artists(&self) -> Option<Vec<&str>> {
self.artist().map(|v| vec![v])
if self.config().parse_multiple_artists {
self.artist()
.map(|a| a.split(self.config().sep_artist).collect::<Vec<&str>>())
} else {
self.artist().map(|v| vec![v])
}
}
fn add_artist(&mut self, artist: &str) {
self.set_artist(artist);
@ -446,7 +477,12 @@ pub trait AudioTagIo {
fn remove_album_artist(&mut self);
fn album_artists(&self) -> Option<Vec<&str>> {
self.artist().map(|v| vec![v])
if self.config().parse_multiple_artists {
self.album_artist()
.map(|a| a.split(self.config().sep_artist).collect::<Vec<&str>>())
} else {
self.album_artist().map(|v| vec![v])
}
}
fn add_album_artist(&mut self, artist: &str) {
self.set_album_artist(artist);
@ -499,11 +535,15 @@ pub trait AudioTagIo {
fn write_to(&mut self, file: &mut File) -> crate::Result<()>;
// cannot use impl AsRef<Path>
fn write_to_path(&mut self, path: &str) -> crate::Result<()>;
}
pub trait AudioTagCommon {
fn config(&self) -> &Config;
fn with_config(&self, config: Config) -> Box<dyn AudioTag>;
fn into_anytag(&self) -> AnyTag<'_>;
/// Convert the tag type, which can be lossy.
fn into_tag(&self, tag_type: TagType) -> Box<dyn AudioTagIo> {
fn into_tag(&self, tag_type: TagType) -> Box<dyn AudioTag> {
match tag_type {
TagType::Id3v2 => Box::new(Id3v2Tag::from(self.into_anytag())),
TagType::Mp4 => Box::new(Mp4Tag::from(self.into_anytag())),
@ -519,23 +559,6 @@ pub trait AudioTagIo {
// }
// }
// pub trait IntoTag: AudioTagIo {
// fn into_tag<'a, T>(&'a self) -> T
// where T: From<AnyTag<'a> {
// self.into_anytag().into()
// }
// }
// impl AnyTag {
// pub fn artists_as_string(&self, sep: &str) -> Option<String> {
// self.artists().map(|artists| artists.join(sep))
// }
// pub fn album_artists_as_string(&self, sep: &str) -> Option<String> {
// self.album_artists().map(|artists| artists.join(sep))
// }
// }
// #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
// pub enum PictureType {
// Other,

View file

@ -1,84 +1,64 @@
use super::*;
use mp4ameta;
pub struct Mp4Tag {
inner: mp4ameta::Tag,
}
use mp4ameta::Tag as InnerTag;
impl Mp4Tag {
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Self> {
Ok(Self {
inner: mp4ameta::Tag::read_from_path(path)?,
})
}
}
impl_tag!(Mp4Tag, InnerTag);
impl<'a> From<&'a Mp4Tag> for AnyTag<'a> {
fn from(inp: &'a Mp4Tag) -> Self {
(&inp.inner).into()
let title = inp.title().map(Cow::borrowed);
let artists = inp
.artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>());
let year = inp.year();
let album_title = inp.album_title().map(Cow::borrowed);
let album_artists = inp
.album_artists()
.map(|i| i.into_iter().map(Cow::borrowed).collect::<Vec<_>>());
let album_cover = inp.album_cover();
let (a, b) = inp.track();
let track_number = a;
let total_tracks = b;
let (a, b) = inp.disc();
let disc_number = a;
let total_discs = b;
Self {
config: inp.config.clone(),
title,
artists,
year,
album_title,
album_cover,
album_artists,
track_number,
total_tracks,
disc_number,
total_discs,
}
}
}
impl<'a> From<AnyTag<'a>> for Mp4Tag {
fn from(inp: AnyTag<'a>) -> Self {
Self { inner: inp.into() }
}
}
impl<'a> From<&'a mp4ameta::Tag> for AnyTag<'a> {
fn from(inp: &'a mp4ameta::Tag) -> Self {
let mut t = Self::default();
t.title = inp.title().map(Cow::borrowed);
let artists = inp.artists().fold(Vec::new(), |mut v, a| {
v.push(Cow::borrowed(a));
v
});
t.artists = if artists.len() > 0 {
Some(artists)
} else {
None
};
if let Some(Ok(y)) = inp.year().map(|y| y.parse()) {
t.year = Some(y);
Self {
config: inp.config.clone(),
inner: {
let mut t = mp4ameta::Tag::default();
inp.title().map(|v| t.set_title(v));
inp.artists()
.map(|i| i.iter().for_each(|a| t.add_artist(a.as_ref())));
inp.year.map(|v| t.set_year(v.to_string()));
inp.album_title().map(|v| t.set_album(v));
inp.album_artists()
.map(|i| i.iter().for_each(|a| t.add_album_artist(a.as_ref())));
inp.track_number().map(|v| t.set_track_number(v));
inp.total_tracks().map(|v| t.set_total_tracks(v));
inp.disc_number().map(|v| t.set_disc_number(v));
inp.total_discs().map(|v| t.set_total_discs(v));
t
},
}
t.album_title = inp.album().map(Cow::borrowed);
let album_artists = inp.album_artists().fold(Vec::new(), |mut v, a| {
v.push(Cow::borrowed(a));
v
});
t.album_artists = if album_artists.len() > 0 {
Some(album_artists)
} else {
None
};
if let Some(Ok(img)) = inp.artwork().map(|a| a.try_into()) {
t.album_cover = Some(img);
}
let (a, b) = inp.track();
t.track_number = a;
t.total_tracks = b;
let (a, b) = inp.disc();
t.disc_number = a;
t.total_discs = b;
t
}
}
impl<'a> From<AnyTag<'a>> for mp4ameta::Tag {
fn from(inp: AnyTag<'a>) -> Self {
let mut t = mp4ameta::Tag::default();
inp.title().map(|v| t.set_title(v));
inp.artists()
.map(|i| i.iter().for_each(|a| t.add_artist(a.as_ref())));
inp.year.map(|v| t.set_year(v.to_string()));
inp.album_title().map(|v| t.set_album(v));
inp.album_artists()
.map(|i| i.iter().for_each(|a| t.add_album_artist(a.as_ref())));
inp.track_number().map(|v| t.set_track_number(v));
inp.total_tracks().map(|v| t.set_total_tracks(v));
inp.disc_number().map(|v| t.set_disc_number(v));
inp.total_discs().map(|v| t.set_total_discs(v));
t
}
}
@ -99,11 +79,7 @@ impl<'a> std::convert::TryFrom<&'a mp4ameta::Data> for Picture<'a> {
}
}
impl AudioTagIo for Mp4Tag {
fn into_anytag(&self) -> AnyTag<'_> {
self.into()
}
impl AudioTag for Mp4Tag {
fn title(&self) -> Option<&str> {
self.inner.title()
}

View file

@ -1,4 +1,4 @@
use audiotags::{Tag, TagType};
use audiotags::{Config, Tag, TagType};
#[test]
fn test_convert_mp3_to_mp4() {
@ -27,6 +27,8 @@ fn test_convert_mp3_to_mp4() {
Some(vec!["artist1 of mp4", "artist2 of mp4"])
);
// convert to id3 tag, which does not support multiple artists
let mp3tag = mp4tag.into_tag(TagType::Id3v2);
assert_eq!(mp3tag.artist(), Some("artist1 of mp4;artist2 of mp4"));
let mp3tag = mp4tag
.with_config(Config::default().sep_artist("/")) // separator is by default `;`
.into_tag(TagType::Id3v2);
assert_eq!(mp3tag.artist(), Some("artist1 of mp4/artist2 of mp4"));
}