VERY rough implementation of a single VorbisTag struct for all formats using vorbis comments, seems to work so far.

This commit is contained in:
Serial 2021-04-06 20:57:30 -04:00
parent c835687098
commit 702f6617c5
17 changed files with 397 additions and 55 deletions

View file

@ -14,13 +14,19 @@ categories = ["accessiblity", "multimedia::audio"]
[dependencies]
opus_headers = {version = "0.1.2", optional = true}
lewton = {version = "0.10.2", optional = true}
metaflac = {version = "0.2.4", optional = true}
mp4ameta = {version = "0.9.1", optional = true}
id3 = {version = "0.6.2", optional = true}
mp3-duration = {version = "0.1.10", optional = true}
hound = {version = "3.4.0", optional = true}
claxon = {version = "0.4.3", optional = true}
thiserror = "1.0.24"
[features]
default = ["tags", "duration"]
tags = ["opus_headers", "lewton", "metaflac", "mp4ameta", "id3"]
default = ["full"]
full = ["all_tags", "duration"]
mp4 = ["mp4ameta"]
mp3 = ["id3"]
wav = ["hound"]
vorbis = ["lewton", "claxon", "opus_headers"]
all_tags = ["vorbis", "mp4", "mp3", "wav"]
duration = ["mp3-duration"]

View file

@ -1,6 +1,8 @@
#![cfg(feature = "flac")]
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture,
Result, TagType, ToAny, ToAnyTag,
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType,
ToAny, ToAnyTag,
};
use std::{convert::TryInto, fs::File, path::Path};

View file

@ -1,10 +1,19 @@
#![cfg(feature = "mp3")]
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
Result, TagType, ToAny, ToAnyTag,
Result, ToAny, ToAnyTag, TagType
};
use std::{convert::TryInto, fs::File, path::Path};
pub use id3::Tag as Id3v2InnerTag;
use crate::traits::ReadPath;
impl ReadPath for Id3v2InnerTag {
fn from_path<P>(path: P, _tag_type: Option<TagType>) -> Result<Self> where P: AsRef<std::path::Path>, Self: Sized {
Ok(Self::read_from_path(path)?)
}
}
impl_tag!(Id3v2Tag, Id3v2InnerTag, TagType::Id3v2);

View file

@ -1,11 +1,11 @@
pub(crate) mod flac_tag;
pub(crate) mod id3_tag;
pub(crate) mod mp4_tag;
pub(crate) mod opus_tag;
pub(crate) mod ogg_tag;
pub(crate) mod vorbis_tag;
pub(crate) mod wav_tag;
pub use flac_tag::FlacTag;
#[cfg(feature = "vorbis")]
pub use vorbis_tag::VorbisTag;
#[cfg(feature = "mp3")]
pub use id3_tag::Id3v2Tag;
pub use mp4_tag::Mp4Tag;
pub use opus_tag::OpusTag;
pub use ogg_tag::OggTag;
#[cfg(feature = "mp4")]
pub use mp4_tag::Mp4Tag;

View file

@ -1,10 +1,19 @@
#![cfg(feature = "mp4")]
use crate::{
impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Error, MimeType, Picture,
Result, TagType, ToAny, ToAnyTag,
Result, ToAny, ToAnyTag, TagType
};
use std::{fs::File, path::Path};
pub use mp4ameta::{FourCC, Tag as Mp4InnerTag};
use crate::traits::ReadPath;
impl ReadPath for Mp4InnerTag {
fn from_path<P>(path: P, _tag_type: Option<TagType>) -> Result<Self> where P: AsRef<std::path::Path>, Self: Sized {
Ok(Self::read_from_path(path)?)
}
}
impl_tag!(Mp4Tag, Mp4InnerTag, TagType::Mp4);

View file

@ -1,5 +1,8 @@
#![cfg(feature = "ogg")]
use crate::{
impl_tag, traits::MissingImplementations, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, TagType, ToAny, ToAnyTag,
impl_tag, traits::MissingImplementations, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite,
Picture, Result, TagType, ToAny, ToAnyTag,
};
use std::{
collections::{hash_map::RandomState, HashMap},

View file

@ -1,11 +1,10 @@
#![cfg(feature = "opus")]
use crate::{
impl_tag, traits::MissingImplementations, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite,
Picture, Result, TagType, ToAny, ToAnyTag,
};
use std::{
fs::File,
path::Path,
};
use std::{fs::File, path::Path};
use opus_headers::{CommentHeader, IdentificationHeader, OpusHeaders as OpusInnerTag};
@ -28,7 +27,7 @@ impl MissingImplementations for OpusInnerTag {
}
}
fn read_from_path<P>(path: P) -> Result<Self>
fn from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
{

View file

@ -0,0 +1,294 @@
#![cfg(feature = "vorbis")]
use crate::{impl_tag, Album, AnyTag, AudioTag, AudioTagEdit, AudioTagWrite, Picture, Result, ToAny, ToAnyTag, TagType};
use std::{fs::File, path::Path, collections::HashMap};
struct VorbisInnerTag {
tag_type: Option<TagType>,
comments: HashMap<String, String>,
}
impl Default for VorbisInnerTag {
fn default() -> Self {
Self {
tag_type: None,
comments: Default::default()
}
}
}
impl VorbisInnerTag {
fn get_value(&self, key: &str) -> Option<&str> {
if let Some(pair) = self.comments.get_key_value(key) {
if !pair.1.is_empty() {
Some(pair.1.as_str())
} else {
None
}
} else {
None
}
}
fn set_value<V>(&mut self, key: &str, val: V) where
V: Into<String> {
let mut comments = self.comments.clone();
let _ = comments.insert(key.to_string(), val.into());
self.comments = comments;
}
fn remove_key(&mut self, key: &str) {
let mut comments = self.comments.clone();
comments.retain(|k, _| k != key);
self.comments = comments;
}
fn from_path<P>(path: P, tag_type: Option<TagType>) -> Result<Self>
where
P: AsRef<Path>,
{
if let Some(tag_type) = tag_type {
match tag_type {
TagType::Ogg => {
let headers = lewton::inside_ogg::OggStreamReader::new(File::open(path)?).unwrap();
let comments: HashMap<String, String> =
headers.comment_hdr.comment_list.into_iter().collect();
Ok(Self {
tag_type: Some(tag_type),
comments,
})
}
TagType::Opus => {
let headers = opus_headers::parse_from_path(path)?;
Ok(Self {
tag_type: Some(tag_type),
comments: headers.comments.user_comments,
})
}
TagType::Flac => {
let headers = claxon::FlacReader::new(File::open(path)?).unwrap();
let comments = headers.tags().map(|(k, v)| (k.to_string(), v.to_string())).collect();
Ok(Self {
tag_type: Some(tag_type),
comments,
})
}
_ => unreachable!()
}
} else {
unreachable!()
}
}
}
impl_tag!(VorbisTag, VorbisInnerTag, TagType::Ogg);
impl<'a> From<AnyTag<'a>> for VorbisTag {
fn from(inp: AnyTag<'a>) -> Self {
let mut t = VorbisTag::default();
inp.title().map(|v| t.set_title(v));
inp.artists_as_string().map(|v| t.set_artist(&v));
inp.year.map(|v| t.set_year(v as u16));
inp.album().title.map(|v| t.set_album_title(v));
inp.album()
.artists
.map(|v| t.set_album_artists(v.join(", ")));
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
}
}
impl<'a> From<&'a VorbisTag> for AnyTag<'a> {
fn from(inp: &'a VorbisTag) -> Self {
let mut t = Self::default();
t.title = inp.title();
t.artists = inp.artists();
t.year = inp.year().map(|y| y as i32);
t.album = Album::new(inp.album_title(), inp.album_artists(), inp.album_cover());
t.track_number = inp.track_number();
t.total_tracks = inp.total_tracks();
t.disc_number = inp.disc_number();
t.total_discs = inp.total_discs();
t
}
}
impl AudioTagEdit for VorbisTag {
fn title(&self) -> Option<&str> {
self.0.get_value("TITLE")
}
fn set_title(&mut self, title: &str) {
self.0.set_value("TITLE", title);
}
fn remove_title(&mut self) {
self.0.remove_key("TITLE");
}
fn artist(&self) -> Option<&str> {
self.0.get_value("ARTIST")
}
fn set_artist(&mut self, artist: &str) {
self.0.set_value("ARTIST", artist)
}
fn add_artist(&mut self, artist: &str) {
todo!()
}
fn artists(&self) -> Option<Vec<&str>> {
self.artist().map(|a| a.split(", ").collect())
}
fn remove_artist(&mut self) {
self.0.remove_key("ARTIST");
}
fn year(&self) -> Option<u16> {
if let Some(Ok(y)) = self.0
.get_value("DATE")
.map(|s| s.chars().take(4).collect::<String>().parse::<i32>())
{
Some(y as u16)
} else if let Some(Ok(y)) = self.0.get_value("YEAR").map(|s| s.parse::<i32>()) {
Some(y as u16)
} else {
None
}
}
fn set_year(&mut self, year: u16) {
self.0.set_value("DATE", &year.to_string());
self.0.set_value("YEAR", &year.to_string());
}
fn remove_year(&mut self) {
self.0.remove_key("YEAR");
self.0.remove_key("DATE");
}
fn album_title(&self) -> Option<&str> {
self.0.get_value("ALBUM")
}
fn set_album_title(&mut self, title: &str) {
self.0.set_value("ALBUM", title)
}
fn remove_album_title(&mut self) {
self.0.remove_key("ALBUM");
}
fn album_artists(&self) -> Option<Vec<&str>> {
self.0.get_value("ALBUMARTIST").map(|a| vec![a])
}
fn set_album_artists(&mut self, artists: String) {
self.0.set_value("ALBUMARTIST", artists)
}
fn add_album_artist(&mut self, artist: &str) {
todo!()
}
fn remove_album_artists(&mut self) {
self.0.remove_key("ALBUMARTIST");
}
fn album_cover(&self) -> Option<Picture> {
// TODO
// self.0
// .pictures()
// .filter(|&pic| matches!(pic.picture_type, metaflac::block::PictureType::CoverFront))
// .next()
// .and_then(|pic| {
// Some(Picture {
// data: &pic.data,
// mime_type: (pic.mime_type.as_str()).try_into().ok()?,
// })
// })
None
}
fn set_album_cover(&mut self, cover: Picture) {
// TODO
// self.remove_album_cover();
// let mime = String::from(cover.mime_type);
// let picture_type = metaflac::block::PictureType::CoverFront;
// self.0
// .add_picture(mime, picture_type, (cover.data).to_owned());
}
fn remove_album_cover(&mut self) {
// TODO
// self.0
// .remove_picture_type(metaflac::block::PictureType::CoverFront)
}
fn track_number(&self) -> Option<u16> {
if let Some(Ok(n)) = self.0.get_value("TRACKNUMBER").map(|x| x.parse::<u16>()) {
Some(n)
} else {
None
}
}
fn set_track_number(&mut self, v: u16) {
self.0.set_value("TRACKNUMBER", &v.to_string())
}
fn remove_track_number(&mut self) {
self.0.remove_key("TRACKNUMBER");
}
// ! not standard
fn total_tracks(&self) -> Option<u16> {
if let Some(Ok(n)) = self.0.get_value("TOTALTRACKS").map(|x| x.parse::<u16>()) {
Some(n)
} else {
None
}
}
fn set_total_tracks(&mut self, v: u16) {
self.0.set_value("TOTALTRACKS", &v.to_string())
}
fn remove_total_tracks(&mut self) {
self.0.remove_key("TOTALTRACKS");
}
fn disc_number(&self) -> Option<u16> {
if let Some(Ok(n)) = self.0.get_value("DISCNUMBER").map(|x| x.parse::<u16>()) {
Some(n)
} else {
None
}
}
fn set_disc_number(&mut self, v: u16) {
self.0.set_value("DISCNUMBER", &v.to_string())
}
fn remove_disc_number(&mut self) {
self.0.remove_key("DISCNUMBER");
}
// ! not standard
fn total_discs(&self) -> Option<u16> {
if let Some(Ok(n)) = self.0.get_value("TOTALDISCS").map(|x| x.parse::<u16>()) {
Some(n)
} else {
None
}
}
fn set_total_discs(&mut self, v: u16) {
self.0.set_value("TOTALDISCS", &v.to_string())
}
fn remove_total_discs(&mut self) {
self.0.remove_key("TOTALDISCS");
}
}
impl AudioTagWrite for VorbisTag {
fn write_to(&mut self, file: &mut File) -> Result<()> {
// self.0.write_to(file)?; TODO
Ok(())
}
fn write_to_path(&mut self, path: &str) -> Result<()> {
// self.0.write_to_path(path)?; TODO
Ok(())
}
}

View file

@ -0,0 +1 @@

View file

@ -12,7 +12,7 @@ pub enum Error {
UnsupportedMimeType(String),
#[error(transparent)]
FlacTagError(#[from] metaflac::Error),
FlacTagError(#[from] claxon::Error),
#[error(transparent)]
Id3TagError(#[from] id3::Error),
#[error(transparent)]

View file

@ -43,14 +43,17 @@
//!
//! # Features
//!
//! By default, `tags` and `duration` are enabled.
//! By default, `full` (`all_tags` and `duration`) are enabled.
//!
//! `tags` provides all the track metadata (`artists`, `album`, etc.) in [AnyTag].
//! `all_tags` provides all the track metadata (`artists`, `album`, etc.) in [AnyTag].
//!
//! `duration` provides the `duration` field in [AnyTag].
//!
//! Either one can be disabled if it doesn't fit your use case.
//!
//! In addition to this, each format can be individually enabled.
//! All features are: `opus, ogg, flac, mp4, mp3, wav`.
//!
//! ## Performance
//!
//! Using lofty incurs a little overhead due to vtables if you want to guess the metadata format (from file extension).
@ -91,7 +94,7 @@ mod error;
pub use crate::error::{Error, Result};
mod components;
pub use crate::components::{FlacTag, Id3v2Tag, Mp4Tag, OpusTag, OggTag};
pub use crate::components::*;
mod traits;
pub use crate::traits::{AudioTag, AudioTagEdit, AudioTagWrite, ToAny, ToAnyTag};

View file

@ -1,8 +1,8 @@
#[doc(hidden)]
#[macro_export]
macro_rules! impl_tag {
($tag:ident , $inner:ident, $tag_type:expr) => {
#[doc(hidden)]
($tag:ident, $inner:ident, $tag_type:expr) => {
#[doc(hidden)]
pub struct $tag($inner);
impl Default for $tag {
@ -15,11 +15,11 @@ macro_rules! impl_tag {
pub fn new() -> Self {
Self::default()
}
pub fn read_from_path<P>(path: P) -> Result<Self>
pub fn read_from_path<P>(path: P, tag_type: Option<TagType>) -> Result<Self>
where
P: AsRef<Path>,
{
Ok(Self($inner::read_from_path(path)?))
Ok(Self($inner::from_path(path, tag_type)?))
}
}
@ -71,14 +71,15 @@ macro_rules! impl_tag {
}
}
}
// From dyn AudioTag to inner (any type)
impl std::convert::From<Box<dyn AudioTag>> for $inner {
impl From<Box<dyn AudioTag>> for $inner {
fn from(inp: Box<dyn AudioTag>) -> Self {
let t: $tag = inp.into();
t.into()
}
}
};
}
}
/// Convert a concrete tag type into another

View file

@ -1,5 +1,6 @@
use super::{AudioTag, Error, FlacTag, Id3v2Tag, Mp4Tag, OpusTag, Result, OggTag};
use super::{components::*, AudioTag, Error, Result};
use std::path::Path;
use crate::vorbis_tag::VorbisTag;
/// A builder for `Box<dyn AudioTag>`. If you do not want a trait object, you can use individual types.
#[derive(Default)]
@ -18,27 +19,33 @@ impl Tag {
pub fn read_from_path(&self, path: impl AsRef<Path>) -> Result<Box<dyn AudioTag>> {
let extension = path.as_ref().extension().unwrap().to_str().unwrap();
match self.0.unwrap_or(TagType::try_from_ext(extension)?) {
TagType::Id3v2 => Ok(Box::new(Id3v2Tag::read_from_path(path)?)),
TagType::Ogg => Ok(Box::new(OggTag::read_from_path(path)?)),
TagType::Opus => Ok(Box::new(OpusTag::read_from_path(path)?)),
TagType::Flac => Ok(Box::new(FlacTag::read_from_path(path)?)),
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path)?)),
match self.0.as_ref().unwrap_or(&TagType::try_from_ext(extension)?) {
#[cfg(feature = "mp3")]
TagType::Id3v2 => Ok(Box::new(Id3v2Tag::read_from_path(path, None)?)),
#[cfg(feature = "mp4")]
TagType::Mp4 => Ok(Box::new(Mp4Tag::read_from_path(path, None)?)),
#[cfg(feature = "vorbis")]
id @ _ => Ok(Box::new(VorbisTag::read_from_path(path, Some(id.to_owned()))?)),
}
}
}
/// The tag type, based on the file extension.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug, PartialEq)]
pub enum TagType {
#[cfg(feature = "mp3")]
/// Common file extensions: `.mp3`
Id3v2,
#[cfg(feature = "vorbis")]
/// Common file extensions: `.ogg, .oga`
Ogg,
#[cfg(feature = "vorbis")]
/// Common file extensions: `.opus`
Opus,
#[cfg(feature = "vorbis")]
/// Common file extensions: `.flac`
Flac,
#[cfg(feature = "mp4")]
/// Common file extensions: `.mp4, .m4a, .m4p, .m4b, .m4r, .m4v`
Mp4,
}
@ -46,10 +53,15 @@ pub enum TagType {
impl TagType {
fn try_from_ext(ext: &str) -> Result<Self> {
match ext {
#[cfg(feature = "mp3")]
"mp3" => Ok(Self::Id3v2),
#[cfg(feature = "vorbis")]
"opus" => Ok(Self::Opus),
#[cfg(feature = "vorbis")]
"flac" => Ok(Self::Flac),
#[cfg(feature = "vorbis")]
"ogg" | "oga" => Ok(Self::Ogg),
#[cfg(feature = "mp4")]
"m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4),
_ => Err(Error::UnsupportedFormat(ext.to_owned())),
}

View file

@ -1,7 +1,6 @@
use crate::{
Album, AnyTag, FlacTag, Id3v2Tag, Mp4Tag, OpusTag, Picture, Result, TagType, OggTag,
};
use std::{fs::File, path::Path};
use crate::{components::*, Album, AnyTag, Picture, Result, TagType};
use std::fs::File;
use crate::vorbis_tag::VorbisTag;
pub trait AudioTag: AudioTagEdit + AudioTagWrite + ToAnyTag {}
@ -103,10 +102,15 @@ pub trait ToAnyTag: ToAny {
// TODO: write a macro or something that implement this method for every tag type so that if the
// TODO: target type is the same, just return self
match tag_type {
#[cfg(feature = "mp3")]
TagType::Id3v2 => Box::new(Id3v2Tag::from(self.to_anytag())),
TagType::Ogg => Box::new(OggTag::from(self.to_anytag())),
TagType::Opus => Box::new(OpusTag::from(self.to_anytag())),
TagType::Flac => Box::new(FlacTag::from(self.to_anytag())),
#[cfg(feature = "vorbis")]
TagType::Ogg => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "vorbis")]
TagType::Opus => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "vorbis")]
TagType::Flac => Box::new(VorbisTag::from(self.to_anytag())),
#[cfg(feature = "mp4")]
TagType::Mp4 => Box::new(Mp4Tag::from(self.to_anytag())),
}
}
@ -117,10 +121,6 @@ pub trait ToAny {
fn to_any_mut(&mut self) -> &mut dyn std::any::Any;
}
pub trait MissingImplementations {
fn default() -> Self;
fn read_from_path<P>(path: P) -> Result<Self>
where
P: AsRef<Path>,
Self: Sized;
}
pub trait ReadPath {
fn from_path<P>(path: P, _tag_type: Option<TagType>) -> Result<Self> where P: AsRef<std::path::Path>, Self: Sized;
}

View file

@ -1,6 +1,5 @@
use crate::Album;
/// The tag returned from `read_from_path`
#[derive(Default)]
pub struct AnyTag<'a> {

View file

@ -1,6 +1,7 @@
use lofty::*;
#[test]
#[cfg(all(feature = "mp3", feature = "flac"))]
fn test_inner() {
let mut innertag = metaflac::Tag::default();
innertag

View file

@ -1,3 +1,4 @@
#![cfg(feature = "default")]
use lofty::{MimeType, Picture, Tag};
macro_rules! test_file {
@ -50,9 +51,11 @@ macro_rules! test_file {
}
test_file!(test_ape, "tests/assets/a.ape");
test_file!(test_flac, "tests/assets/a.flac");
test_file!(test_m4a, "tests/assets/a.m4a");
test_file!(test_mp3, "tests/assets/a.mp3");
test_file!(test_wav, "tests/assets/a.wav");
// Vorbis comments
test_file!(test_flac, "tests/assets/a.flac");
test_file!(test_ogg, "tests/assets/a.ogg");
test_file!(test_opus, "tests/assets/a.opus");
test_file!(test_wav, "tests/assets/a.wav");