audiotags/
lib.rs

1//! [![Crate](https://img.shields.io/crates/v/audiotags.svg)](https://crates.io/crates/audiotags)
2//! [![Crate](https://img.shields.io/crates/d/audiotags.svg)](https://crates.io/crates/audiotags)
3//! [![Crate](https://img.shields.io/crates/l/audiotags.svg)](https://crates.io/crates/audiotags)
4//! [![Documentation](https://docs.rs/audiotags/badge.svg)](https://docs.rs/audiotags/)
5//!
6//! This crate makes it easier to parse, convert and write metadata (a.k.a tag) in audio files of different file types.
7//!
8//! This crate aims to provide a unified trait for parsers and writers of different audio file formats.
9//! This means that you can parse tags in mp3, flac, and m4a files with a single function: `Tag::default().
10//! read_from_path()` and get fields by directly calling `.album()`, `.artist()` on its result. Without this
11//! crate, you would otherwise need to learn different APIs in **id3**, **mp4ameta** etc. in order to parse
12//! metadata in different file formats.
13//!
14//! ## Performance
15//!
16//! Using **audiotags** incurs a little overhead due to vtables if you want to guess the metadata format
17//! (from file extension). Apart from this the performance is almost the same as directly calling function
18//! provided by those 'specialized' crates.
19//!
20//! No copies will be made if you only need to read and write metadata of one format. If you want to convert
21//! between tags, copying is unavoidable no matter if you use **audiotags** or use getters and setters provided
22//! by specialized libraries. **audiotags** is not making additional unnecessary copies.
23//!
24//! ## Supported Formats
25//!
26//! | File Format   | Metadata Format       | backend                                                     |
27//! |---------------|-----------------------|-------------------------------------------------------------|
28//! | `mp3`         | id3v2.4               | [**id3**](https://github.com/polyfloyd/rust-id3)            |
29//! | `m4a/mp4/...` | MPEG-4 audio metadata | [**mp4ameta**](https://github.com/Saecki/rust-mp4ameta)     |
30//! | `flac`        | Vorbis comment        | [**metaflac**](https://github.com/jameshurst/rust-metaflac) |
31//!
32//! ## Examples
33//!
34//! Read the [manual](https://docs.rs/audiotags) for some examples, but here's a quick-one:
35//!
36//! ```rust,no_run
37//! use audiotags::{Tag, Picture, MimeType};
38//!
39//! // using `default()` or `new()` alone so that the metadata format is
40//! // guessed (from the file extension) (in this case, Id3v2 tag is read)
41//! let mut tag = Tag::new().read_from_path("test.mp3").unwrap();
42//!
43//! tag.set_title("foo title");
44//! assert_eq!(tag.title(), Some("foo title"));
45//! tag.remove_title();
46//! assert!(tag.title().is_none());
47//! tag.remove_title();
48//! // trying to remove a field that's already empty won't hurt
49//!
50//! let cover = Picture {
51//!     mime_type: MimeType::Jpeg,
52//!     data: &vec![0u8; 10],
53//! };
54//!
55//! tag.set_album_cover(cover.clone());
56//! assert_eq!(tag.album_cover(), Some(cover));
57//! tag.remove_album_cover();
58//! assert!(tag.album_cover().is_none());
59//! tag.remove_album_cover();
60//!
61//! tag.write_to_path("test.mp3").expect("Fail to save");
62//! ```
63
64pub(crate) use audiotags_macro::*;
65
66pub mod anytag;
67pub use anytag::*;
68
69pub mod components;
70pub use components::*;
71
72pub mod error;
73pub use error::{Error, Result};
74
75pub mod traits;
76pub use traits::*;
77
78pub mod types;
79pub use types::*;
80
81pub mod config;
82pub use config::Config;
83
84use std::convert::From;
85use std::fs::File;
86use std::path::Path;
87
88pub use std::convert::{TryFrom, TryInto};
89
90/// A builder for `Box<dyn AudioTag>`. If you do not want a trait object, you can use individual types.
91///
92/// # Examples
93///
94/// ```no_run
95/// use audiotags::{Tag, TagType};
96///
97/// # fn main() -> audiotags::Result<()> {
98/// // Guess the format by default
99/// let mut tag = Tag::new().read_from_path("assets/a.mp3").unwrap();
100/// tag.set_title("Foo");
101///
102/// // you can convert the tag type and save the metadata to another file.
103/// tag.to_dyn_tag(TagType::Mp4).write_to_path("assets/a.m4a")?;
104///
105/// // you can specify the tag type (but when you want to do this, also consider directly using the concrete type)
106/// let tag = Tag::new().with_tag_type(TagType::Mp4).read_from_path("assets/a.m4a").unwrap();
107/// assert_eq!(tag.title(), Some("Foo"));
108/// # Ok(()) }
109/// ```
110#[derive(Default)]
111pub struct Tag {
112    /// The tag type which can be specified with `.with_tag_type()` before parsing.
113    tag_type: Option<TagType>,
114    /// The config which can be specified with `.with_config()` before parsing.
115    config: Config,
116}
117
118impl Tag {
119    /// Initiate a new Tag (a builder for `Box<dyn AudioTag>`) with default configurations.
120    /// You can then optionally chain `with_tag_type` and/or `with_config`.
121    /// Finally, you `read_from_path`
122    pub fn new() -> Self {
123        Self::default()
124    }
125    /// Specify the tag type
126    pub fn with_tag_type(self, tag_type: TagType) -> Self {
127        Self {
128            tag_type: Some(tag_type),
129            config: self.config,
130        }
131    }
132    /// Specify configuration, if you do not want to use the default
133    pub fn with_config(self, config: Config) -> Self {
134        Self {
135            tag_type: self.tag_type,
136            config,
137        }
138    }
139    pub fn read_from_path(
140        &self,
141        path: impl AsRef<Path>,
142    ) -> crate::Result<Box<dyn AudioTag + Send + Sync>> {
143        match self.tag_type.unwrap_or(TagType::try_from_ext(
144            path.as_ref()
145                .extension()
146                .ok_or(Error::UnknownFileExtension(String::new()))?
147                .to_string_lossy()
148                .to_string()
149                .to_lowercase()
150                .as_str(),
151        )?) {
152            TagType::Id3v2 => Ok(Box::new({
153                let mut t = Id3v2Tag::read_from_path(path)?;
154                t.set_config(self.config);
155                t
156            })),
157            TagType::Mp4 => Ok(Box::new({
158                let mut t = Mp4Tag::read_from_path(path)?;
159                t.set_config(self.config);
160                t
161            })),
162            TagType::Flac => Ok(Box::new({
163                let mut t = FlacTag::read_from_path(path)?;
164                t.set_config(self.config);
165                t
166            })),
167        }
168    }
169}
170
171#[derive(Clone, Copy, Debug)]
172pub enum TagType {
173    /// ## Common file extensions
174    ///
175    /// `.mp3`
176    ///
177    /// ## References
178    ///
179    /// - <https://www.wikiwand.com/en/ID3>
180    Id3v2,
181    Flac,
182    /// ## Common file extensions
183    ///
184    /// `.mp4, .m4a, .m4p, .m4b, .m4r and .m4v`
185    ///
186    /// ## References
187    ///
188    /// - <https://www.wikiwand.com/en/MPEG-4_Part_14>
189    Mp4,
190}
191
192#[rustfmt::skip]
193impl TagType {
194    fn try_from_ext(ext: &str) -> crate::Result<Self> {
195        match ext {
196                                                     "mp3" => Ok(Self::Id3v2),
197            "m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Self::Mp4),
198                                                    "flac" => Ok(Self::Flac),
199            p => Err(crate::Error::UnsupportedFormat(p.to_owned())),
200        }
201    }
202}
203
204/// Convert a concrete tag type into another
205#[macro_export]
206macro_rules! convert {
207    ($inp:expr, $target_type:ty) => {
208        $target_type::from(inp.to_anytag())
209    };
210}