This commit is contained in:
Tianyi 2020-10-25 14:58:50 +00:00
parent c43090f291
commit 57171bd561
19 changed files with 3247 additions and 0 deletions

5
.gitignore vendored
View file

@ -8,3 +8,8 @@ Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
*.mp3
*.m4a
src/main.rs
.lib1.rs

15
Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "audiotags"
version = "0.0.1"
authors = ["Tianyi <ShiTianyi2001@outlook.com>"]
edition = "2018"
description = "Unified IO for different types of audio metadata"
license = "MIT"
repository = "https://github.com/TianyiShi2001/audiotags"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
id3 = "0.5.1"
mp4ameta = {path = "./rust-mp4ameta"}
strum = {version = "0.19.5", features = ["derive"]}

43
README.md Normal file
View file

@ -0,0 +1,43 @@
# audiotags
This crate makes it easier to parse tags/metadata in audio files of different file types.
This crate aims to provide a unified trait for parsers and writers of different audio file formats. This means that you can parse tags in mp3 and m4a files with a single function: `audiotags::from_path()` and get fields by directly calling `.album()`, `.artist()` on its result. Without this crate, you would otherwise need to learn different APIs in **id3**, **mp4ameta** crates in order to parse metadata in different file foramts.
## Example
```rust
use audiotags;
fn main() {
const MP3: &'static str = "a.mp3";
let mut tags = audiotags::from_path(MP3).unwrap();
// without this crate you would call id3::Tag::from_path()
println!("Title: {:?}", tags.title());
println!("Artist: {:?}", tags.artist());
tags.set_album_artist("CINDERELLA PROJECT");
let album = tags.album().unwrap();
println!("Album title and artist: {:?}", (album.title, album.artist));
println!("Track: {:?}", tags.track());
tags.write_to_path(MP3).unwrap();
// Title: Some("お願い!シンデレラ")
// Artist: Some("高垣楓、城ヶ崎美嘉、小日向美穂、十時愛梨、川島瑞樹、日野茜、輿水幸子、佐久間まゆ、白坂小梅")
// Album title and artist: ("THE IDOLM@STER CINDERELLA GIRLS ANIMATION PROJECT 01 Star!!", Some("CINDERELLA PROJECT"))
// Track: (Some(2), Some(4))
const M4A: &'static str = "b.m4a";
let mut tags = audiotags::from_path(M4A).unwrap();
// without this crate you would call mp4ameta::Tag::from_path()
println!("Title: {:?}", tags.title());
println!("Artist: {:?}", tags.artist());
let album = tags.album().unwrap();
println!("Album title and artist: {:?}", (album.title, album.artist));
tags.set_total_tracks(4);
println!("Track: {:?}", tags.track());
tags.write_to_path(M4A).unwrap();
// Title: Some("ふわふわ時間")
// Artist: Some("桜高軽音部 [平沢唯・秋山澪・田井中律・琴吹紬(CV:豊崎愛生、日笠陽子、佐藤聡美、寿美菜子)]")
// Album title and artist: ("ふわふわ時間", Some("桜高軽音部 [平沢唯・秋山澪・田井中律・琴吹紬(CV:豊崎愛生、日笠陽子、佐藤聡美、寿美菜子)]"))
// Track: (Some(1), Some(4))
}
```

19
rust-mp4ameta/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "mp4ameta"
version = "0.5.2"
authors = ["Saecki <tobiasschmitz2001@gmail.com>"]
license = "MIT OR Apache-2.0"
readme = "README.md"
description = "A library for reading and writing iTunes style MPEG-4 audio metadata."
categories = ["multimedia::audio", "parsing"]
keywords = ["mp4", "m4a", "audio", "metadata", "parser"]
repository = "https://github.com/Saecki/rust-mp4ameta"
edition = "2018"
[dependencies]
byteorder = "1.3.4"
lazy_static = "1.4.0"
mp4ameta_proc = { path = "mp4ameta_proc", version = "0.1.0" }
[dev-dependencies]
walkdir = "2.3.1"

View file

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 Tobias Schmitz
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

21
rust-mp4ameta/LICENSE-MIT Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Tobias Schmitz
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

46
rust-mp4ameta/README.md Normal file
View file

@ -0,0 +1,46 @@
# rust-mp4ameta
[![CI](https://github.com/Saecki/rust-mp4ameta/workflows/CI/badge.svg)](https://github.com/Saecki/rust-mp4ameta/actions?query=workflow%3ACI)
[![Crate](https://img.shields.io/crates/v/mp4ameta.svg)](https://crates.io/crates/mp4ameta)
[![Documentation](https://docs.rs/mp4ameta/badge.svg)](https://docs.rs/mp4ameta)
![License](https://img.shields.io/crates/l/mp4ameta?color=blue)
![LOC](https://tokei.rs/b1/github/saecki/rust-mp4ameta?category=code)
A library for reading and writing iTunes style MPEG-4 audio metadata.
## Usage
```rust
fn main() {
let mut tag = mp4ameta::Tag::read_from_path("music.m4a").unwrap();
println!("{}", tag.artist().unwrap());
tag.set_artist("artist");
tag.write_to_path("music.m4a").unwrap();
}
```
## Supported Filetypes
- M4A
- M4B
- M4P
- M4V
## Useful Links
- [AtomicParsley Doc](http://atomicparsley.sourceforge.net/mpeg-4files.html)
- [Mutagen Doc](https://mutagen.readthedocs.io/en/latest/api/mp4.html)
- QuickTime Spec
- [Movie Atoms](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html)
- [Metadata](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html)
## Testing
__Running all tests:__
```
cargo test -- --test-threads=1
```
__To test this library against your collection symlink your music dir into the `files` dir and run:__
```
cargo test sample_files -- --show-output
```

View file

@ -0,0 +1,2 @@
/target
Cargo.lock

View file

@ -0,0 +1,11 @@
[package]
name = "mp4ameta_proc"
version = "0.1.1"
authors = ["Saecki <tobiasschmitz2001@gmail.com>"]
license = "MIT OR Apache-2.0"
description = "Procedural macros to generate common accessors for the mp4ameta crate."
repository = "https://github.com/Saecki/rust-mp4ameta"
edition = "2018"
[lib]
proc-macro = true

View file

@ -0,0 +1,209 @@
use proc_macro::TokenStream;
fn base_values(input: TokenStream) -> (String, String, String, String, String) {
let input_string = input.to_string();
let mut token_strings = input_string.split(',');
let value_ident = token_strings.next().expect("Expected function ident").trim_start().replace("\"", "");
let name = value_ident.replace('_', " ");
let mut name_chars = name.chars();
let headline = format!("{}{}", name_chars.next().unwrap().to_uppercase(), name_chars.collect::<String>());
let atom_ident = format!("atom::{}", value_ident.to_uppercase());
let atom_ident_string = token_strings.next().expect("Expected atom ident string").trim_start().replace("\"", "");
(value_ident,
name,
headline,
atom_ident,
atom_ident_string)
}
#[proc_macro]
pub fn individual_string_value_accessor(input: TokenStream) -> TokenStream {
let (value_ident,
name,
headline,
atom_ident,
atom_ident_string)
= base_values(input);
format!("
/// ### {0}
impl Tag {{
/// Returns the {1} (`{2}`).
pub fn {3}(&self) -> Option<&str> {{
self.string({4}).next()
}}
/// Sets the {1} (`{2}`).
pub fn set_{3}(&mut self, {3}: impl Into<String>) {{
self.set_data({4}, Data::Utf8({3}.into()));
}}
/// Removes the {1} (`{2}`).
pub fn remove_{3}(&mut self) {{
self.remove_data({4});
}}
}}
",
headline,
name,
atom_ident_string,
value_ident,
atom_ident,
).parse().expect("Error parsing accessor impl block:")
}
#[proc_macro]
pub fn multiple_string_values_accessor(input: TokenStream) -> TokenStream {
let (value_ident,
name,
headline,
atom_ident,
atom_ident_string)
= base_values(input);
let mut value_ident_plural = value_ident.clone();
if value_ident_plural.ends_with('y') {
let _ = value_ident_plural.split_off(value_ident_plural.len());
value_ident_plural.push_str("ies");
} else {
value_ident_plural.push('s');
};
let name_plural = value_ident_plural.replace('_', " ");
format!("
/// ### {0}
impl Tag {{
/// Returns all {2} (`{3}`).
pub fn {5}(&self) -> impl Iterator<Item=&str> {{
self.string({6})
}}
/// Returns the first {1} (`{3}`).
pub fn {4}(&self) -> Option<&str> {{
self.string({6}).next()
}}
/// Sets the {1} (`{3}`). This will remove all other {2}.
pub fn set_{4}(&mut self, {4}: impl Into<String>) {{
self.set_data({6}, Data::Utf8({4}.into()));
}}
/// Adds an {1} (`{3}`).
pub fn add_{4}(&mut self, {4}: impl Into<String>) {{
self.add_data({6}, Data::Utf8({4}.into()));
}}
/// Removes all {2} (`{3}`).
pub fn remove_{5}(&mut self) {{
self.remove_data({6});
}}
}}
",
headline,
name,
name_plural,
atom_ident_string,
value_ident,
value_ident_plural,
atom_ident,
).parse().expect("Error parsing accessor impl block:")
}
#[proc_macro]
pub fn flag_value_accessor(input: TokenStream) -> TokenStream {
let (value_ident,
name,
headline,
atom_ident,
atom_ident_string)
= base_values(input);
format!("
/// ### {0}
impl Tag {{
/// Returns the {1} flag (`{2}`).
pub fn {3}(&self) -> bool {{
let vec = match self.data({4}).next() {{
Some(Data::Reserved(v)) => v,
Some(Data::BeSigned(v)) => v,
_ => return false,
}};
if vec.is_empty() {{
return false;
}}
vec[0] != 0
}}
/// Sets the {1} flag to true (`{2}`).
pub fn set_{3}(&mut self) {{
self.set_data({4}, Data::BeSigned(vec![1u8]));
}}
/// Removes the {1} flag (`{2}`).
pub fn remove_{3}(&mut self) {{
self.remove_data({4})
}}
}}
",
headline,
name,
atom_ident_string,
value_ident,
atom_ident,
).parse().expect("Error parsing accessor impl block:")
}
#[proc_macro]
pub fn integer_value_accessor(input: TokenStream) -> TokenStream {
let (value_ident,
name,
headline,
atom_ident,
atom_ident_string)
= base_values(input);
format!("
/// ### {0}
impl Tag {{
/// Returns the {1} (`{2}`)
pub fn {3}(&self) -> Option<u16> {{
let vec = match self.data({4}).next()? {{
Data::Reserved(v) => v,
Data::BeSigned(v) => v,
_ => return None,
}};
if vec.len() < 2 {{
return None;
}}
Some(u16::from_be_bytes([vec[0], vec[1]]))
}}
/// Sets the {1} (`{2}`)
pub fn set_{3}(&mut self, {3}: u16) {{
let vec: Vec<u8> = {3}.to_be_bytes().to_vec();
self.set_data({4}, Data::BeSigned(vec));
}}
/// Removes the {1} (`{2}`).
pub fn remove_{3}(&mut self) {{
self.remove_data({4});
}}
}}
",
headline,
name,
atom_ident_string,
value_ident,
atom_ident,
).parse().expect("Error parsing accessor impl block:")
}

View file

@ -0,0 +1,736 @@
use std::fmt::{Debug, Display, Formatter, Result};
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::ops::Deref;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crate::{data, Content, ContentT, Data, DataT, ErrorKind, Tag};
/// A list of valid file types defined by the `ftyp` atom.
pub const VALID_FILETYPES: [&str; 6] = ["M4A ", "M4B ", "M4P ", "M4V ", "isom", "mp4"];
/// (`ftyp`) Identifier of an atom information about the filetype.
pub const FILETYPE: Ident = Ident(*b"ftyp");
/// (`moov`) Identifier of an atom containing a structure of children storing metadata.
pub const MOVIE: Ident = Ident(*b"moov");
/// (`trak`) Identifier of an atom containing information about a single track.
pub const TRACK: Ident = Ident(*b"trak");
/// (`mdia`) Identifier of an atom containing information about a tracks media type and data.
pub const MEDIA: Ident = Ident(*b"mdia");
/// (`mdhd`) Identifier of an atom specifying the characteristics of a media atom.
pub const MEDIA_HEADER: Ident = Ident(*b"mdhd");
/// (`udta`) Identifier of an atom containing user metadata.
pub const USER_DATA: Ident = Ident(*b"udta");
/// (`meta`) Identifier of an atom containing a metadata item list.
pub const METADATA: Ident = Ident(*b"meta");
/// (`ilst`) Identifier of an atom containing a list of metadata atoms.
pub const ITEM_LIST: Ident = Ident(*b"ilst");
/// (`data`) Identifier of an atom containing typed data.
pub const DATA: Ident = Ident(*b"data");
// iTunes 4.0 atoms
/// (`©alb`)
pub const ALBUM: Ident = Ident(*b"\xa9alb");
/// (`aART`)
pub const ALBUM_ARTIST: Ident = Ident(*b"aART");
/// (`©ART`)
pub const ARTIST: Ident = Ident(*b"\xa9ART");
/// (`covr`)
pub const ARTWORK: Ident = Ident(*b"covr");
/// (`tmpo`)
pub const BPM: Ident = Ident(*b"tmpo");
/// (`©cmt`)
pub const COMMENT: Ident = Ident(*b"\xa9cmt");
/// (`cpil`)
pub const COMPILATION: Ident = Ident(*b"cpil");
/// (`©wrt`)
pub const COMPOSER: Ident = Ident(*b"\xa9wrt");
/// (`cprt`)
pub const COPYRIGHT: Ident = Ident(*b"cprt");
/// (`©gen`)
pub const CUSTOM_GENRE: Ident = Ident(*b"\xa9gen");
/// (`disk`)
pub const DISC_NUMBER: Ident = Ident(*b"disk");
/// (`©too`)
pub const ENCODER: Ident = Ident(*b"\xa9too");
/// (`rtng`)
pub const ADVISORY_RATING: Ident = Ident(*b"rtng");
/// (`gnre`)
pub const STANDARD_GENRE: Ident = Ident(*b"gnre");
/// (`©nam`)
pub const TITLE: Ident = Ident(*b"\xa9nam");
/// (`trkn`)
pub const TRACK_NUMBER: Ident = Ident(*b"trkn");
/// (`©day`)
pub const YEAR: Ident = Ident(*b"\xa9day");
// iTunes 4.2 atoms
/// (`©grp`)
pub const GROUPING: Ident = Ident(*b"\xa9grp");
/// (`stik`)
pub const MEDIA_TYPE: Ident = Ident(*b"stik");
// iTunes 4.9 atoms
/// (`catg`)
pub const CATEGORY: Ident = Ident(*b"catg");
/// (`keyw`)
pub const KEYWORD: Ident = Ident(*b"keyw");
/// (`pcst`)
pub const PODCAST: Ident = Ident(*b"pcst");
/// (`egid`)
pub const PODCAST_EPISODE_GLOBAL_UNIQUE_ID: Ident = Ident(*b"egid");
/// (`purl`)
pub const PODCAST_URL: Ident = Ident(*b"purl");
// iTunes 5.0
/// (`desc`)
pub const DESCRIPTION: Ident = Ident(*b"desc");
/// (`©lyr`)
pub const LYRICS: Ident = Ident(*b"\xa9lyr");
// iTunes 6.0
/// (`tves`)
pub const TV_EPISODE: Ident = Ident(*b"tves");
/// (`tven`)
pub const TV_EPISODE_NUMBER: Ident = Ident(*b"tven");
/// (`tvnn`)
pub const TV_NETWORK_NAME: Ident = Ident(*b"tvnn");
/// (`tvsn`)
pub const TV_SEASON: Ident = Ident(*b"tvsn");
/// (`tvsh`)
pub const TV_SHOW_NAME: Ident = Ident(*b"tvsh");
// iTunes 6.0.2
/// (`purd`)
pub const PURCHASE_DATE: Ident = Ident(*b"purd");
// iTunes 7.0
/// (`pgap`)
pub const GAPLESS_PLAYBACK: Ident = Ident(*b"pgap");
// Work, Movement
/// (`©mvn`)
pub const MOVEMENT: Ident = Ident(*b"\xa9mvn");
/// (`©mvc`)
pub const MOVEMENT_COUNT: Ident = Ident(*b"\xa9mvc");
/// (`©mvi`)
pub const MOVEMENT_INDEX: Ident = Ident(*b"\xa9mvi");
/// (`©wrk`)
pub const WORK: Ident = Ident(*b"\xa9wrk");
/// (`shwm`)
pub const SHOW_MOVEMENT: Ident = Ident(*b"shwm");
lazy_static! {
/// Lazily initialized static reference to a `ftyp` atom template.
pub static ref FILETYPE_ATOM_T: AtomT = filetype_atom_t();
/// Lazily initialized static reference to an atom metadata hierarchy template needed to parse
/// metadata.
pub static ref ITEM_LIST_ATOM_T: AtomT = item_list_atom_t();
/// Lazily initialized static reference to an atom hierarchy template leading to an empty `ilst`
/// atom.
pub static ref METADATA_ATOM_T: AtomT = metadata_atom_t();
}
/// A 4 byte atom identifier.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ident(pub [u8; 4]);
impl Deref for Ident {
type Target = [u8; 4];
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl Display for Ident {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"{}",
self.0.iter().map(|b| char::from(*b)).collect::<String>()
)
}
}
/// A struct that represents a MPEG-4 audio metadata atom.
#[derive(Clone, PartialEq)]
pub struct Atom {
/// The 4 byte identifier of the atom.
pub ident: Ident,
/// The offset in bytes separating the head from the content.
pub offset: usize,
/// The content of an atom.
pub content: Content,
}
impl Debug for Atom {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"Atom{{ {}, {}, {:#?} }}",
self.ident, self.offset, self.content
)
}
}
impl Atom {
/// Creates an atom containing the provided content at a n byte offset.
pub fn with(ident: Ident, offset: usize, content: Content) -> Self {
Self {
ident,
offset,
content,
}
}
/// Creates an atom with the `identifier`, containing
/// [`Content::RawData`](enum.Content.html#variant.RawData) with the provided `data`.
pub fn with_raw_data(ident: Ident, offset: usize, data: Data) -> Self {
Self::with(ident, offset, Content::RawData(data))
}
/// Creates an atom with the `identifier`, containing
/// [`Content::TypedData`](enum.Content.html#variant.TypedData) with the provided `data`.
pub fn with_typed_data(ident: Ident, offset: usize, data: Data) -> Self {
Self::with(ident, offset, Content::TypedData(data))
}
/// Creates a data atom containing [`Content::TypedData`](enum.Content.html#variant.TypedData)
/// with the provided `data`.
pub fn data_atom_with(data: Data) -> Self {
Self::with(DATA, 0, Content::TypedData(data))
}
/// Returns the length of the atom in bytes.
pub fn len(&self) -> usize {
8 + self.offset + self.content.len()
}
/// Returns true if the atom has no `offset` or `content` and only consists of it's 8 byte head.
pub fn is_empty(&self) -> bool {
self.offset + self.content.len() == 0
}
/// Returns a reference to the first children atom matching the `identifier`, if present.
pub fn child(&self, ident: Ident) -> Option<&Self> {
if let Content::Atoms(v) = &self.content {
for a in v {
if a.ident == ident {
return Some(a);
}
}
}
None
}
/// Returns a mutable reference to the first children atom matching the `identifier`, if
/// present.
pub fn mut_child(&mut self, ident: Ident) -> Option<&mut Self> {
if let Content::Atoms(v) = &mut self.content {
for a in v {
if a.ident == ident {
return Some(a);
}
}
}
None
}
/// Return a reference to the first children atom, if present.
pub fn first_child(&self) -> Option<&Self> {
match &self.content {
Content::Atoms(v) => v.first(),
_ => None,
}
}
/// Returns a mutable reference to the first children atom, if present.
pub fn mut_first_child(&mut self) -> Option<&mut Self> {
match &mut self.content {
Content::Atoms(v) => v.first_mut(),
_ => None,
}
}
/// Attempts to write the atom to the writer.
pub fn write_to(&self, writer: &mut impl Write) -> crate::Result<()> {
writer.write_u32::<BigEndian>(self.len() as u32)?;
writer.write_all(&*self.ident)?;
writer.write_all(&vec![0u8; self.offset])?;
self.content.write_to(writer)?;
Ok(())
}
/// Checks if the filetype is valid, returns an error otherwise.
pub fn check_filetype(&self) -> crate::Result<()> {
match &self.content {
Content::RawData(Data::Utf8(s)) => {
for f in &VALID_FILETYPES {
if s.starts_with(f) {
return Ok(());
}
}
Err(crate::Error::new(
ErrorKind::InvalidFiletype(s.clone()),
"Invalid filetype.".into(),
))
}
_ => Err(crate::Error::new(
ErrorKind::NoTag,
"No filetype atom found.".into(),
)),
}
}
}
/// A template representing a MPEG-4 audio metadata atom.
#[derive(Clone, PartialEq)]
pub struct AtomT {
/// The 4 byte identifier of the atom.
pub ident: Ident,
/// The offset in bytes separating the head from the content.
pub offset: usize,
/// The content template of an atom template.
pub content: ContentT,
}
impl Debug for AtomT {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
write!(
f,
"Atom{{ {}, {}, {:#?} }}",
self.ident, self.offset, self.content
)
}
}
impl AtomT {
/// Creates an atom template containing the provided content at a n byte offset.
pub fn with(ident: Ident, offset: usize, content: ContentT) -> Self {
Self {
ident,
offset,
content,
}
}
/// Creates an atom template containing [`ContentT::RawData`](enum.ContentT.html#variant.RawData)
/// with the provided data template.
pub fn with_raw_data(ident: Ident, offset: usize, data: DataT) -> Self {
Self::with(ident, offset, ContentT::RawData(data))
}
/// Creates a data atom template containing [`ContentT::TypedData`](enum.ContentT.html#variant.TypedData).
pub fn data_atom() -> Self {
Self::with(DATA, 0, ContentT::TypedData)
}
/// Returns a reference to the first children atom template matching the identifier, if present.
pub fn child(&self, ident: Ident) -> Option<&Self> {
if let ContentT::Atoms(v) = &self.content {
for a in v {
if a.ident == ident {
return Some(a);
}
}
}
None
}
/// Returns a mutable reference to the first children atom template matching the identifier, if
/// present.
pub fn mut_child(&mut self, ident: Ident) -> Option<&mut Self> {
if let ContentT::Atoms(v) = &mut self.content {
for a in v {
if a.ident == ident {
return Some(a);
}
}
}
None
}
/// Returns a reference to the first children atom template, if present.
pub fn first_child(&self) -> Option<&Self> {
match &self.content {
ContentT::Atoms(v) => v.first(),
_ => None,
}
}
/// Returns a mutable reference to the first children atom template, if present.
pub fn mut_first_child(&mut self) -> Option<&mut Self> {
match &mut self.content {
ContentT::Atoms(v) => v.first_mut(),
_ => None,
}
}
/// Attempts to parse an atom, that matches the template, from the `reader`. This should only be
/// used if the atom has to be in this exact position, if the parsed and expected `identifier`s
/// don't match this will return an error.
pub fn parse_next(&self, reader: &mut (impl Read + Seek)) -> crate::Result<Atom> {
let (length, ident) = match parse_head(reader) {
Ok(h) => h,
Err(e) => return Err(e),
};
if ident == self.ident {
match self.parse_content(reader, length) {
Ok(c) => Ok(Atom::with(self.ident, self.offset, c)),
Err(e) => Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", ident, e.description),
)),
}
} else {
Err(crate::Error::new(
ErrorKind::AtomNotFound(self.ident),
format!("Expected {} found {}", self.ident, ident),
))
}
}
/// Attempts to parse an atom, that matches the template, from the reader.
pub fn parse(&self, reader: &mut (impl Read + Seek)) -> crate::Result<Atom> {
let current_position = reader.seek(SeekFrom::Current(0))?;
let complete_length = reader.seek(SeekFrom::End(0))?;
let length = (complete_length - current_position) as usize;
reader.seek(SeekFrom::Start(current_position))?;
let mut parsed_bytes = 0;
while parsed_bytes < length {
let (atom_length, atom_ident) = parse_head(reader)?;
if atom_ident == self.ident {
return match self.parse_content(reader, atom_length) {
Ok(c) => Ok(Atom::with(self.ident, self.offset, c)),
Err(e) => Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", atom_ident, e.description),
)),
};
} else {
reader.seek(SeekFrom::Current((atom_length - 8) as i64))?;
}
parsed_bytes += atom_length;
}
Err(crate::Error::new(
ErrorKind::AtomNotFound(self.ident),
format!("No {} atom found", self.ident),
))
}
/// Attempts to parse the atom template's content from the reader.
pub fn parse_content(
&self,
reader: &mut (impl Read + Seek),
length: usize,
) -> crate::Result<Content> {
if length > 8 {
if self.offset != 0 {
reader.seek(SeekFrom::Current(self.offset as i64))?;
}
self.content.parse(reader, length - 8 - self.offset)
} else {
Ok(Content::Empty)
}
}
}
/// Attempts to read MPEG-4 audio metadata from the reader.
pub fn read_tag_from(reader: &mut (impl Read + Seek)) -> crate::Result<Tag> {
let mut tag_atoms = Vec::with_capacity(5);
let mut tag_readonly_atoms = Vec::with_capacity(2);
let ftyp = FILETYPE_ATOM_T.parse_next(reader)?;
ftyp.check_filetype()?;
tag_readonly_atoms.push(ftyp);
let moov = METADATA_ATOM_T.parse(reader)?;
if let Some(trak) = moov.child(TRACK) {
if let Some(mdia) = trak.child(MEDIA) {
if let Some(mdhd) = mdia.child(MEDIA_HEADER) {
tag_readonly_atoms.push(mdhd.clone());
}
}
}
if let Some(udta) = moov.child(USER_DATA) {
if let Some(meta) = udta.first_child() {
if let Some(ilst) = meta.first_child() {
if let Content::Atoms(atoms) = &ilst.content {
tag_atoms = atoms.to_vec();
}
}
}
}
Ok(Tag::with(tag_atoms, tag_readonly_atoms))
}
/// Attempts to write the metadata atoms to the file inside the item list atom.
pub fn write_tag_to(file: &File, atoms: &[Atom]) -> crate::Result<()> {
let mut reader = BufReader::new(file);
let mut writer = BufWriter::new(file);
let mut atom_pos_and_len = Vec::new();
let mut destination = &item_list_atom_t();
let ftyp = FILETYPE_ATOM_T.parse_next(&mut reader)?;
ftyp.check_filetype()?;
while let Ok((length, ident)) = parse_head(&mut reader) {
if ident == destination.ident {
let pos = reader.seek(SeekFrom::Current(0))? as usize - 8;
atom_pos_and_len.push((pos, length));
reader.seek(SeekFrom::Current(destination.offset as i64))?;
match destination.first_child() {
Some(a) => destination = a,
None => break,
}
} else {
reader.seek(SeekFrom::Current(length as i64 - 8))?;
}
}
let old_file_length = reader.seek(SeekFrom::End(0))?;
let metadata_position = atom_pos_and_len[atom_pos_and_len.len() - 1].0 + 8;
let old_metadata_length = atom_pos_and_len[atom_pos_and_len.len() - 1].1 - 8;
let new_metadata_length = atoms.iter().map(|a| a.len()).sum::<usize>();
let metadata_length_difference = new_metadata_length as i32 - old_metadata_length as i32;
// reading additional data after metadata
let mut additional_data =
Vec::with_capacity(old_file_length as usize - (metadata_position + old_metadata_length));
reader.seek(SeekFrom::Start(
(metadata_position + old_metadata_length) as u64,
))?;
reader.read_to_end(&mut additional_data)?;
// adjusting the file length
file.set_len((old_file_length as i64 + metadata_length_difference as i64) as u64)?;
// adjusting the atom lengths
for (pos, len) in atom_pos_and_len {
writer.seek(SeekFrom::Start(pos as u64))?;
writer.write_u32::<BigEndian>((len as i32 + metadata_length_difference) as u32)?;
}
// writing metadata
writer.seek(SeekFrom::Current(4))?;
for a in atoms {
a.write_to(&mut writer)?;
}
// writing additional data after metadata
writer.write_all(&additional_data)?;
writer.flush()?;
Ok(())
}
/// Attempts to dump the metadata atoms to the writer. This doesn't include a complete MPEG-4
/// container hierarchy and won't result in a usable file.
pub fn dump_tag_to(writer: &mut impl Write, atoms: Vec<Atom>) -> crate::Result<()> {
let ftyp = Atom::with(
FILETYPE,
0,
Content::RawData(Data::Utf8("M4A \u{0}\u{0}\u{2}\u{0}isomiso2".into())),
);
let moov = Atom::with(
MOVIE,
0,
Content::atoms().add_atom_with(
USER_DATA,
0,
Content::atoms().add_atom_with(
METADATA,
4,
Content::atoms().add_atom_with(ITEM_LIST, 0, Content::Atoms(atoms)),
),
),
);
ftyp.write_to(writer)?;
moov.write_to(writer)?;
Ok(())
}
/// Attempts to parse the list of atoms, matching the templates, from the reader.
pub fn parse_atoms(
atoms: &[AtomT],
reader: &mut (impl Read + Seek),
length: usize,
) -> crate::Result<Vec<Atom>> {
let mut parsed_bytes = 0;
let mut parsed_atoms = Vec::with_capacity(atoms.len());
while parsed_bytes < length {
let (atom_length, atom_ident) = parse_head(reader)?;
let mut parsed = false;
for a in atoms {
if atom_ident == a.ident {
match a.parse_content(reader, atom_length) {
Ok(c) => {
parsed_atoms.push(Atom::with(a.ident, a.offset, c));
parsed = true;
}
Err(e) => {
return Err(crate::Error::new(
e.kind,
format!("Error reading {}: {}", atom_ident, e.description),
));
}
}
break;
}
}
if atom_length > 8 && !parsed {
reader.seek(SeekFrom::Current((atom_length - 8) as i64))?;
}
parsed_bytes += atom_length;
}
Ok(parsed_atoms)
}
/// Attempts to parse the atom's head containing a 32 bit unsigned integer determining the size
/// of the atom in bytes and the following 4 byte identifier from the reader.
pub fn parse_head(reader: &mut (impl Read + Seek)) -> crate::Result<(usize, Ident)> {
let length = match reader.read_u32::<BigEndian>() {
Ok(l) => l as usize,
Err(e) => {
return Err(crate::Error::new(
ErrorKind::Io(e),
"Error reading atom length".into(),
));
}
};
let mut ident = [0u8; 4];
if let Err(e) = reader.read_exact(&mut ident) {
return Err(crate::Error::new(
ErrorKind::Io(e),
"Error reading atom identifier".into(),
));
}
Ok((length, Ident(ident)))
}
/// Returns an `ftyp` atom template needed to parse the filetype.
fn filetype_atom_t() -> AtomT {
AtomT::with_raw_data(FILETYPE, 0, DataT::with(data::UTF8))
}
/// Returns an atom metadata hierarchy template needed to parse metadata.
fn metadata_atom_t() -> AtomT {
AtomT::with(
MOVIE,
0,
ContentT::atoms_t()
.add_atom_t_with(
TRACK,
0,
ContentT::atoms_t().add_atom_t_with(
MEDIA,
0,
ContentT::atoms_t().add_atom_t_with(
MEDIA_HEADER,
0,
ContentT::RawData(DataT::with(data::RESERVED)),
),
),
)
.add_atom_t_with(
USER_DATA,
0,
ContentT::atoms_t().add_atom_t_with(
METADATA,
4,
ContentT::atoms_t().add_atom_t_with(
ITEM_LIST,
0,
ContentT::atoms_t()
.add_atom_t_with(ADVISORY_RATING, 0, ContentT::data_atom_t())
.add_atom_t_with(ALBUM, 0, ContentT::data_atom_t())
.add_atom_t_with(ALBUM_ARTIST, 0, ContentT::data_atom_t())
.add_atom_t_with(ARTIST, 0, ContentT::data_atom_t())
.add_atom_t_with(BPM, 0, ContentT::data_atom_t())
.add_atom_t_with(CATEGORY, 0, ContentT::data_atom_t())
.add_atom_t_with(COMMENT, 0, ContentT::data_atom_t())
.add_atom_t_with(COMPILATION, 0, ContentT::data_atom_t())
.add_atom_t_with(COMPOSER, 0, ContentT::data_atom_t())
.add_atom_t_with(COPYRIGHT, 0, ContentT::data_atom_t())
.add_atom_t_with(CUSTOM_GENRE, 0, ContentT::data_atom_t())
.add_atom_t_with(DESCRIPTION, 0, ContentT::data_atom_t())
.add_atom_t_with(DISC_NUMBER, 0, ContentT::data_atom_t())
.add_atom_t_with(ENCODER, 0, ContentT::data_atom_t())
.add_atom_t_with(GAPLESS_PLAYBACK, 0, ContentT::data_atom_t())
.add_atom_t_with(GROUPING, 0, ContentT::data_atom_t())
.add_atom_t_with(KEYWORD, 0, ContentT::data_atom_t())
.add_atom_t_with(LYRICS, 0, ContentT::data_atom_t())
.add_atom_t_with(MEDIA_TYPE, 0, ContentT::data_atom_t())
.add_atom_t_with(MOVEMENT_COUNT, 0, ContentT::data_atom_t())
.add_atom_t_with(MOVEMENT_INDEX, 0, ContentT::data_atom_t())
.add_atom_t_with(MOVEMENT, 0, ContentT::data_atom_t())
.add_atom_t_with(PODCAST, 0, ContentT::data_atom_t())
.add_atom_t_with(
PODCAST_EPISODE_GLOBAL_UNIQUE_ID,
0,
ContentT::data_atom_t(),
)
.add_atom_t_with(PODCAST_URL, 0, ContentT::data_atom_t())
.add_atom_t_with(PURCHASE_DATE, 0, ContentT::data_atom_t())
.add_atom_t_with(SHOW_MOVEMENT, 0, ContentT::data_atom_t())
.add_atom_t_with(STANDARD_GENRE, 0, ContentT::data_atom_t())
.add_atom_t_with(TITLE, 0, ContentT::data_atom_t())
.add_atom_t_with(TRACK_NUMBER, 0, ContentT::data_atom_t())
.add_atom_t_with(TV_EPISODE, 0, ContentT::data_atom_t())
.add_atom_t_with(TV_EPISODE_NUMBER, 0, ContentT::data_atom_t())
.add_atom_t_with(TV_NETWORK_NAME, 0, ContentT::data_atom_t())
.add_atom_t_with(TV_SEASON, 0, ContentT::data_atom_t())
.add_atom_t_with(TV_SHOW_NAME, 0, ContentT::data_atom_t())
.add_atom_t_with(WORK, 0, ContentT::data_atom_t())
.add_atom_t_with(YEAR, 0, ContentT::data_atom_t())
.add_atom_t_with(ARTWORK, 0, ContentT::data_atom_t()),
),
),
),
)
}
/// Returns an atom hierarchy leading to an empty `ilst` atom template.
fn item_list_atom_t() -> AtomT {
AtomT::with(
MOVIE,
0,
ContentT::atom_t_with(
USER_DATA,
0,
ContentT::atom_t_with(
METADATA,
4,
ContentT::atom_t_with(ITEM_LIST, 0, ContentT::atoms_t()),
),
),
)
}

View file

@ -0,0 +1,208 @@
use std::fmt::{Debug, Formatter, Result};
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, ReadBytesExt};
use crate::{Atom, AtomT, core::atom, Data, DataT, ErrorKind, Ident};
/// An enum representing the different types of content an atom might have.
#[derive(Clone, PartialEq)]
pub enum Content {
/// A value containing a list of children atoms.
Atoms(Vec<Atom>),
/// A value containing raw data.
RawData(Data),
/// A value containing data defined by a
/// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34)
/// code.
TypedData(Data),
/// Empty content.
Empty,
}
impl Debug for Content {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
Content::Atoms(a) => write!(f, "Content::Atoms{{ {:#?} }}", a),
Content::RawData(d) => write!(f, "Content::RawData{{ {:?} }}", d),
Content::TypedData(d) => write!(f, "Content::TypedData{{ {:?} }}", d),
Content::Empty => write!(f, "Content::Empty"),
}
}
}
impl Content {
/// Creates new empty content of type [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn atoms() -> Self {
Self::Atoms(Vec::new())
}
/// Creates new content of type [Self::Atoms](enum.Content.html#variant.Atoms) containing the
/// atom.
pub fn atom(atom: Atom) -> Self {
Self::Atoms(vec![atom])
}
/// Creates new content of type [Self::Atoms](enum.Content.html#variant.Atoms) containing a
/// data [`Atom`](struct.Atom.html) with the data.
pub fn data_atom_with(data: Data) -> Self {
Self::atom(Atom::data_atom_with(data))
}
/// Creates new content of type [Self::Atoms](Content::Atoms) containing a new
/// [`Atom`](struct.Atom.html) with the identifier, offset and content.
pub fn atom_with(ident: Ident, offset: usize, content: Self) -> Self {
Self::atom(Atom::with(ident, offset, content))
}
/// Adds the atom to the list of children atoms if `self` is of type [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn add_atom(self, atom: Atom) -> Self {
if let Self::Atoms(mut atoms) = self {
atoms.push(atom);
Self::Atoms(atoms)
} else {
self
}
}
/// Adds a new [`Atom`](struct.Atom.html) with the provided `identifier`, `offset` and `content`
/// to the list of children if `self` is of type [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn add_atom_with(self, ident: Ident, offset: usize, content: Self) -> Self {
self.add_atom(Atom::with(ident, offset, content))
}
/// Returns the length in bytes.
pub fn len(&self) -> usize {
match self {
Self::Atoms(v) => v.iter().map(|a| a.len()).sum(),
Self::RawData(d) => d.len(),
Self::TypedData(d) => 8 + d.len(),
Self::Empty => 0,
}
}
/// Returns true if the content is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Attempts to write the content to the `writer`.
pub fn write_to(&self, writer: &mut impl Write) -> crate::Result<()> {
match self {
Self::Atoms(v) => {
for a in v {
a.write_to(writer)?;
}
}
Self::RawData(d) => d.write_raw(writer)?,
Self::TypedData(d) => d.write_typed(writer)?,
Self::Empty => (),
}
Ok(())
}
}
/// A template representing the different types of content an atom template might have.
#[derive(Clone, PartialEq)]
pub enum ContentT {
/// A
Atoms(Vec<AtomT>),
/// A value containing a data template specifying the datatype.
RawData(DataT),
/// A template representing typed data that is defined by a
/// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34)
/// code prior to the data parsed.
TypedData,
/// Empty content.
Empty,
}
impl Debug for ContentT {
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
match self {
ContentT::Atoms(a) => write!(f, "ContentT::Atoms{{ {:#?} }}", a),
ContentT::RawData(d) => write!(f, "ContentT::RawData{{ {:?} }}", d),
ContentT::TypedData => write!(f, "ContentT::TypedData"),
ContentT::Empty => write!(f, "ContentT::Empty"),
}
}
}
impl ContentT {
/// Creates a new empty content template of type [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn atoms_t() -> Self {
Self::Atoms(Vec::new())
}
/// Creates a new content template of type [Self::Atoms](enum.Content.html#variant.Atoms)
/// containing the `atom` template.
pub fn atom_t(atom: AtomT) -> Self {
Self::Atoms(vec![atom])
}
/// Creates a new content template of type [Self::Atoms](enum.Content.html#variant.Atoms)
/// containing a data atom template.
pub fn data_atom_t() -> Self {
Self::atom_t(AtomT::data_atom())
}
/// Creates a new content template of type [Self::Atoms](enum.Content.html#variant.Atoms)
/// containing a new atom template with the `identifier`, `offset` and `content`.
pub fn atom_t_with(ident: Ident, offset: usize, content: Self) -> Self {
Self::atom_t(AtomT::with(ident, offset, content))
}
/// Adds the atom template to the list of children atom templates if `self` is of type
/// [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn add_atom_t(self, atom: AtomT) -> Self {
if let Self::Atoms(mut atoms) = self {
atoms.push(atom);
Self::Atoms(atoms)
} else {
self
}
}
/// Adds a data atom template to the list of children if `self` is of type
/// [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn add_data_atom_t(self) -> Self {
self.add_atom_t(AtomT::data_atom())
}
/// Adds a new atom template with the provided `identifier`, `offset` and `content` template to
/// the list of children, if `self` is of type [Self::Atoms](enum.Content.html#variant.Atoms).
pub fn add_atom_t_with(self, ident: Ident, offset: usize, content: Self) -> Self {
self.add_atom_t(AtomT::with(ident, offset, content))
}
/// Attempts to parse corresponding content from the `reader`.
pub fn parse(&self, reader: &mut (impl Read + Seek), length: usize) -> crate::Result<Content> {
Ok(match self {
ContentT::Atoms(v) => Content::Atoms(atom::parse_atoms(v, reader, length)?),
ContentT::RawData(d) => Content::RawData(d.parse(reader, length)?),
ContentT::TypedData => {
if length >= 8 {
let datatype = match reader.read_u32::<BigEndian>() {
Ok(d) => d,
Err(e) => return Err(crate::Error::new(
crate::ErrorKind::Io(e),
"Error reading typed data head".into(),
)),
};
// Skipping 4 byte locale indicator
reader.seek(SeekFrom::Current(4))?;
Content::TypedData(DataT::with(datatype).parse(reader, length - 8)?)
} else {
return Err(crate::Error::new(
ErrorKind::Parsing,
"Typed data head to short".into(),
));
}
}
ContentT::Empty => Content::Empty,
})
}
}

View file

@ -0,0 +1,248 @@
use core::fmt;
use std::io::{Read, Seek, SeekFrom, Write};
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
use crate::ErrorKind;
// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34) codes
/// Reserved for use where no type needs to be indicated.
#[allow(dead_code)]
pub const RESERVED: u32 = 0;
/// UTF-8 without any count or NULL terminator.
#[allow(dead_code)]
pub const UTF8: u32 = 1;
/// UTF-16 also known as UTF-16BE.
#[allow(dead_code)]
pub const UTF16: u32 = 2;
/// UTF-8 variant storage of a string for sorting only.
#[allow(dead_code)]
pub const UTF8_SORT: u32 = 4;
/// UTF-16 variant storage of a string for sorting only.
#[allow(dead_code)]
pub const UTF16_SORT: u32 = 5;
/// JPEG in a JFIF wrapper.
#[allow(dead_code)]
pub const JPEG: u32 = 13;
/// PNG in a PNG wrapper.
#[allow(dead_code)]
pub const PNG: u32 = 14;
/// A big-endian signed integer in 1,2,3 or 4 bytes.
#[allow(dead_code)]
pub const BE_SIGNED: u32 = 21;
/// A big-endian unsigned integer in 1,2,3 or 4 bytes.
#[allow(dead_code)]
pub const BE_UNSIGNED: u32 = 22;
/// A big-endian 32-bit floating point value (`IEEE754`).
#[allow(dead_code)]
pub const BE_F32: u32 = 23;
/// A big-endian 64-bit floating point value (`IEEE754`).
#[allow(dead_code)]
pub const BE_F64: u32 = 24;
/// Windows bitmap format graphics.
#[allow(dead_code)]
pub const BMP: u32 = 27;
/// QuickTime Metadata atom.
#[allow(dead_code)]
pub const QT_META: u32 = 28;
/// An 8-bit signed integer.
#[allow(dead_code)]
pub const I8: u32 = 65;
/// A big-endian 16-bit signed integer.
#[allow(dead_code)]
pub const BE_I16: u32 = 66;
/// A big-endian 32-bit signed integer.
#[allow(dead_code)]
pub const BE_I32: u32 = 67;
/// A block of data representing a two dimensional (2D) point with 32-bit big-endian floating point
/// x and y coordinates. It has the structure:<br/>
/// `{ BE_F32 x; BE_F32 y; }`
#[allow(dead_code)]
pub const BE_POINT_F32: u32 = 70;
/// A block of data representing 2D dimensions with 32-bit big-endian floating point width and
/// height. It has the structure:<br/>
/// `{ width: BE_F32, height: BE_F32 }`
#[allow(dead_code)]
pub const BE_DIMS_F32: u32 = 71;
/// A block of data representing a 2D rectangle with 32-bit big-endian floating point x and y
/// coordinates and a 32-bit big-endian floating point width and height size. It has the structure:<br/>
/// `{ x: BE_F32, y: BE_F32, width: BE_F32, height: BE_F32 }`<br/>
/// or the equivalent structure:<br/>
/// `{ origin: BE_Point_F32, size: BE_DIMS_F32 }`
#[allow(dead_code)]
pub const BE_RECT_F32: u32 = 72;
/// A big-endian 64-bit signed integer.
#[allow(dead_code)]
pub const BE_I64: u32 = 74;
/// An 8-bit unsigned integer.
#[allow(dead_code)]
pub const U8: u32 = 75;
/// A big-endian 16-bit unsigned integer.
#[allow(dead_code)]
pub const BE_U16: u32 = 76;
/// A big-endian 32-bit unsigned integer.
#[allow(dead_code)]
pub const BE_U32: u32 = 77;
/// A big-endian 64-bit unsigned integer.
#[allow(dead_code)]
pub const BE_U64: u32 = 78;
/// A block of data representing a 3x3 transformation matrix. It has the structure:<br/>
/// `{ matrix: [[BE_F64; 3]; 3] }`
#[allow(dead_code)]
pub const AFFINE_TRANSFORM_F64: u32 = 79;
/// An enum that holds different types of data defined by
/// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34).
#[derive(Clone, PartialEq)]
pub enum Data {
/// A value containing reserved type data inside a `Vec<u8>`.
Reserved(Vec<u8>),
/// A value containing a `String` decoded from, or to be encoded to utf-8.
Utf8(String),
/// A value containing a `String` decoded from, or to be encoded to utf-16.
Utf16(String),
/// A value containing jpeg byte data inside a `Vec<u8>`.
Jpeg(Vec<u8>),
/// A value containing png byte data inside a `Vec<u8>`.
Png(Vec<u8>),
/// A value containing big endian signed integer inside a `Vec<u8>`.
BeSigned(Vec<u8>),
}
impl fmt::Debug for Data {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Data::Reserved(d) => write!(f, "Reserved{{ {:?} }}", d),
Data::Utf8(d) => write!(f, "UTF8{{ {:?} }}", d),
Data::Utf16(d) => write!(f, "UTF16{{ {:?} }}", d),
Data::Jpeg(_) => write!(f, "JPEG"),
Data::Png(_) => write!(f, "PNG"),
Data::BeSigned(d) => write!(f, "Reserved{{ {:?} }}", d),
}
}
}
impl Data {
/// Returns the length in bytes.
pub fn len(&self) -> usize {
match self {
Data::Reserved(v) => v.len(),
Data::Utf8(s) => s.len(),
Data::Utf16(s) => s.encode_utf16().count(),
Data::Jpeg(v) => v.len(),
Data::Png(v) => v.len(),
Data::BeSigned(v) => v.len(),
}
}
/// Returns true if the data is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Attempts to write the typed data to the writer.
pub fn write_typed(&self, writer: &mut impl Write) -> crate::Result<()> {
let datatype = match self {
Data::Reserved(_) => RESERVED,
Data::Utf8(_) => UTF8,
Data::Utf16(_) => UTF16,
Data::Jpeg(_) => JPEG,
Data::Png(_) => PNG,
Data::BeSigned(_) => BE_SIGNED,
};
writer.write_u32::<BigEndian>(datatype)?;
// Writing 4 byte locale indicator
writer.write_u32::<BigEndian>(0)?;
self.write_raw(writer)?;
Ok(())
}
/// Attempts to write the raw data to the writer.
pub fn write_raw(&self, writer: &mut impl Write) -> crate::Result<()> {
match self {
Data::Reserved(v) => { writer.write_all(v)?; }
Data::Utf8(s) => { writer.write_all(s.as_bytes())?; }
Data::Utf16(s) => {
for c in s.encode_utf16() {
writer.write_u16::<BigEndian>(c)?;
}
}
Data::Jpeg(v) => { writer.write_all(v)?; }
Data::Png(v) => { writer.write_all(v)?; }
Data::BeSigned(v) => { writer.write_all(v)?; }
}
Ok(())
}
}
/// A template used for parsing data defined by
/// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34).
#[derive(Clone, Debug, PartialEq)]
pub struct DataT {
/// A datatype defined by
/// [Table 3-5 Well-known data types](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/Metadata/Metadata.html#//apple_ref/doc/uid/TP40000939-CH1-SW34).
datatype: u32,
}
impl DataT {
/// Creates a data template containing the datatype.
pub fn with(datatype: u32) -> Self {
DataT { datatype }
}
/// Attempts to parse corresponding data from the reader.
pub fn parse(&self, reader: &mut (impl Read + Seek), length: usize) -> crate::Result<Data> {
Ok(match self.datatype {
RESERVED => Data::Reserved(read_u8_vec(reader, length)?),
UTF8 => Data::Utf8(read_utf8(reader, length)?),
UTF16 => Data::Utf16(read_utf16(reader, length)?),
JPEG => Data::Jpeg(read_u8_vec(reader, length)?),
PNG => Data::Png(read_u8_vec(reader, length)?),
BE_SIGNED => Data::BeSigned(read_u8_vec(reader, length)?),
_ => return Err(crate::Error::new(
ErrorKind::UnknownDataType(self.datatype),
"Unknown datatype code".into(),
)),
})
}
}
/// Attempts to read 8 bit unsigned integers from the reader to a vector of size length.
pub fn read_u8_vec(reader: &mut (impl Read + Seek), length: usize) -> crate::Result<Vec<u8>> {
let mut buf = vec![0u8; length];
reader.read_exact(&mut buf)?;
Ok(buf)
}
/// Attempts to read 16 bit unsigned integers from the reader to a vector of size length.
pub fn read_u16_vec(reader: &mut (impl Read + Seek), length: usize) -> crate::Result<Vec<u16>> {
let mut buf = vec![0u16; length];
reader.read_u16_into::<BigEndian>(&mut buf)?;
Ok(buf)
}
/// Attempts to read a utf-8 string from the reader.
pub fn read_utf8(reader: &mut (impl Read + Seek), length: usize) -> crate::Result<String> {
let data = read_u8_vec(reader, length)?;
Ok(String::from_utf8(data)?)
}
/// Attempts to read a utf-16 string from the reader.
pub fn read_utf16(reader: &mut (impl Read + Seek), length: usize) -> crate::Result<String> {
let data = read_u16_vec(reader, length / 2)?;
if length % 2 == 1 {
reader.seek(SeekFrom::Current(1))?;
}
Ok(String::from_utf16(&data)?)
}

View file

@ -0,0 +1,7 @@
/// Contains constants, structs and functions for working with MPEG-4 metadata atoms.
pub mod atom;
pub mod content;
/// Contains cosntants, structs and functions for working with data held inside MPEG-4 metadata atoms.
pub mod data;
/// Contains structs and constants for working with types held inside data atoms.
pub mod types;

View file

@ -0,0 +1,118 @@
use std::convert::TryFrom;
use crate::ErrorKind;
// iTunes media types
/// A media type code stored in the `stik` atom.
pub const MOVIE: u8 = 0;
/// A media type code stored in the `stik` atom.
pub const NORMAL: u8 = 1;
/// A media type code stored in the `stik` atom.
pub const AUDIOBOOK: u8 = 2;
/// A media type code stored in the `stik` atom.
pub const WHACKED_BOOKMARK: u8 = 5;
/// A media type code stored in the `stik` atom.
pub const MUSIC_VIDEO: u8 = 6;
/// A media type code stored in the `stik` atom.
pub const SHORT_FILM: u8 = 9;
/// A media type code stored in the `stik` atom.
pub const TV_SHOW: u8 = 10;
/// A media type code stored in the `stik` atom.
pub const BOOKLET: u8 = 11;
// iTunes advisory ratings
/// An advisory rating code stored in the `rtng` atom.
pub const CLEAN: u8 = 2;
/// An advisory rating code stored in the `rtng` atom.
pub const INOFFENSIVE: u8 = 0;
/// An enum describing the media type of a file stored in the `stik` atom.
#[derive(Debug, Clone, PartialEq)]
pub enum MediaType {
/// A media type stored as 0 in the `stik` atom.
Movie,
/// A media type stored as 1 in the `stik` atom.
Normal,
/// A media type stored as 2 in the `stik` atom.
AudioBook,
/// A media type stored as 5 in the `stik` atom.
WhackedBookmark,
/// A media type stored as 6 in the `stik` atom.
MusicVideo,
/// A media type stored as 9 in the `stik` atom.
ShortFilm,
/// A media type stored as 10 in the `stik` atom.
TvShow,
/// A media type stored as 11 in the `stik` atom.
Booklet,
}
impl MediaType {
/// Returns the integer value corresponding to the media type.
pub fn value(&self) -> u8 {
match self {
Self::Movie => MOVIE,
Self::Normal => NORMAL,
Self::AudioBook => AUDIOBOOK,
Self::WhackedBookmark => WHACKED_BOOKMARK,
Self::MusicVideo => MUSIC_VIDEO,
Self::ShortFilm => SHORT_FILM,
Self::TvShow => TV_SHOW,
Self::Booklet => BOOKLET,
}
}
}
impl TryFrom<u8> for MediaType {
type Error = crate::Error;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
MOVIE => Ok(Self::Movie),
NORMAL => Ok(Self::Normal),
AUDIOBOOK => Ok(Self::AudioBook),
WHACKED_BOOKMARK => Ok(Self::WhackedBookmark),
MUSIC_VIDEO => Ok(Self::MusicVideo),
SHORT_FILM => Ok(Self::ShortFilm),
TV_SHOW => Ok(Self::TvShow),
BOOKLET => Ok(Self::Booklet),
_ => Err(crate::Error::new(
ErrorKind::UnknownMediaType(value),
"Unknown media type".into(),
)),
}
}
}
/// An enum describing the rating of a file stored in the `rtng` atom.
#[derive(Debug, Clone, PartialEq)]
pub enum AdvisoryRating {
/// An advisory rating stored as 2 in the `rtng` atom.
Clean,
/// An advisory rating stored as 0 in the `rtng` atom.
Inoffensive,
/// An advisory rating indicated by any other value than 0 or 2 in the `rtng` atom, containing
/// the value.
Explicit(u8),
}
impl AdvisoryRating {
/// Returns the integer value corresponding to the rating.
pub fn value(&self) -> u8 {
match self {
Self::Clean => CLEAN,
Self::Inoffensive => INOFFENSIVE,
Self::Explicit(r) => *r,
}
}
}
impl From<u8> for AdvisoryRating {
fn from(rating: u8) -> Self {
match rating {
CLEAN => Self::Clean,
INOFFENSIVE => Self::Inoffensive,
_ => Self::Explicit(rating),
}
}
}

106
rust-mp4ameta/src/error.rs Normal file
View file

@ -0,0 +1,106 @@
use std::{error, fmt, io, string};
use crate::Ident;
/// Type alias for the result of tag operations.
pub type Result<T> = std::result::Result<T, Error>;
/// Kinds of errors that may occur while performing metadata operations.
#[derive(Debug)]
pub enum ErrorKind {
/// An error kind indicating that an atom could not be found. Contains the atom's identifier.
AtomNotFound(Ident),
/// An error kind indicating that an IO error has occurred. Contains the original `io::Error`.
Io(io::Error),
/// An error kind indicating that the filetype read from the ftyp atom was invalid. Contains
/// the invalid filetype string.
InvalidFiletype(String),
/// An error kind indicating that the reader does not contain mp4 metadata.
NoTag,
/// An error kind indicating that an error occurred during parsing.
Parsing,
/// An error kind indicating that the datatype integer describing the typed data is unknown.
/// Contains the unknown datatype.
UnknownDataType(u32),
/// An error kind indicating that the data can't be written to a file.
UnWritableDataType,
/// An error kind indicating that a string decoding error has occurred. Contains the invalid
/// data.
Utf8StringDecoding(string::FromUtf8Error),
/// An error kind indicating that a string decoding error has occurred.
Utf16StringDecoding(string::FromUtf16Error),
/// An error kind indicating that the media type integer is unknown.
/// Contains the unknown media type.
UnknownMediaType(u8),
}
/// A structure able to represent any error that may occur while performing metadata operations.
pub struct Error {
/// The kind of error.
pub kind: ErrorKind,
/// A human readable string describing the error.
pub description: String,
}
impl Error {
/// Creates a new `Error` using the error kind and description.
pub fn new(kind: ErrorKind, description: String) -> Error {
Error { kind, description }
}
}
impl error::Error for Error {
fn cause(&self) -> Option<&dyn error::Error> {
match self.kind {
ErrorKind::Io(ref err) => Some(err),
_ => None,
}
}
}
impl From<io::Error> for Error {
fn from(err: io::Error) -> Error {
Error {
kind: ErrorKind::Io(err),
description: "".into(),
}
}
}
impl From<string::FromUtf8Error> for Error {
fn from(err: string::FromUtf8Error) -> Error {
Error {
kind: ErrorKind::Utf8StringDecoding(err),
description: "Data is not valid utf-8.".into(),
}
}
}
impl From<string::FromUtf16Error> for Error {
fn from(err: string::FromUtf16Error) -> Error {
Error {
kind: ErrorKind::Utf16StringDecoding(err),
description: "Data is not valid utf-16.".into(),
}
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.description.is_empty() {
write!(f, "{:?}", self.kind)
} else {
write!(f, "{:?}: {}", self.kind, self.description)
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if self.description.is_empty() {
write!(f, "{:?}", self.kind)
} else {
write!(f, "{:?}: {}", self.kind, self.description)
}
}
}

45
rust-mp4ameta/src/lib.rs Normal file
View file

@ -0,0 +1,45 @@
//! A library for reading and writing iTunes style MPEG-4 audio metadata.
//!
//! # Example
//!
//! ```no_run
//! let mut tag = mp4ameta::Tag::read_from_path("music.m4a").unwrap();
//!
//! println!("{}", tag.artist().unwrap());
//!
//! tag.set_artist("artist");
//!
//! tag.write_to_path("music.m4a").unwrap();
//! ```
#![warn(missing_docs)]
#[macro_use]
extern crate lazy_static;
pub use crate::core::{
atom,
atom::Atom,
atom::AtomT,
atom::Ident,
content::Content,
content::ContentT,
data,
data::Data,
data::DataT,
types,
types::AdvisoryRating,
types::MediaType,
};
pub use crate::error::{
Error,
ErrorKind,
Result,
};
pub use crate::tag::{
STANDARD_GENRES,
Tag,
};
mod core;
mod error;
mod tag;

839
rust-mp4ameta/src/tag.rs Normal file
View file

@ -0,0 +1,839 @@
use std::convert::TryFrom;
use std::fmt::Debug;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, Read, Seek, Write};
use std::path::Path;
use crate::{AdvisoryRating, Atom, atom, Content, Data, Ident, MediaType};
/// A list of standard genre codes and values found in the `gnre` atom. This list is equal to the
/// ID3v1 genre list but all codes are incremented by 1.
pub const STANDARD_GENRES: [(u16, &str); 80] = [
(1, "Blues"),
(2, "Classic rock"),
(3, "Country"),
(4, "Dance"),
(5, "Disco"),
(6, "Funk"),
(7, "Grunge"),
(8, "Hip,-Hop"),
(9, "Jazz"),
(10, "Metal"),
(11, "New Age"),
(12, "Oldies"),
(13, "Other"),
(14, "Pop"),
(15, "Rhythm and Blues"),
(16, "Rap"),
(17, "Reggae"),
(18, "Rock"),
(19, "Techno"),
(20, "Industrial"),
(21, "Alternative"),
(22, "Ska"),
(23, "Death metal"),
(24, "Pranks"),
(25, "Soundtrack"),
(26, "Euro-Techno"),
(27, "Ambient"),
(28, "Trip-Hop"),
(29, "Vocal"),
(30, "Jazz & Funk"),
(31, "Fusion"),
(32, "Trance"),
(33, "Classical"),
(34, "Instrumental"),
(35, "Acid"),
(36, "House"),
(37, "Game"),
(38, "Sound clip"),
(39, "Gospel"),
(40, "Noise"),
(41, "Alternative Rock"),
(42, "Bass"),
(43, "Soul"),
(44, "Punk"),
(45, "Space"),
(46, "Meditative"),
(47, "Instrumental Pop"),
(48, "Instrumental Rock"),
(49, "Ethnic"),
(50, "Gothic"),
(51, "Darkwave"),
(52, "Techno-Industrial"),
(53, "Electronic"),
(54, "Pop-Folk"),
(55, "Eurodance"),
(56, "Dream"),
(57, "Southern Rock"),
(58, "Comedy"),
(59, "Cult"),
(60, "Gangsta"),
(61, "Top 41"),
(62, "Christian Rap"),
(63, "Pop/Funk"),
(64, "Jungle"),
(65, "Native US"),
(66, "Cabaret"),
(67, "New Wave"),
(68, "Psychedelic"),
(69, "Rave"),
(70, "Show tunes"),
(71, "Trailer"),
(72, "Lo,-Fi"),
(73, "Tribal"),
(74, "Acid Punk"),
(75, "Acid Jazz"),
(76, "Polka"),
(77, "Retro"),
(78, "Musical"),
(79, "Rock n Roll"),
(80, "Hard Rock"),
];
/// A MPEG-4 audio tag containing metadata atoms
#[derive(Default, Debug, Clone, PartialEq)]
pub struct Tag {
/// A vector containing metadata atoms
pub atoms: Vec<Atom>,
/// A vector containing readonly metadata atoms
pub readonly_atoms: Vec<Atom>,
}
impl Tag {
/// Creates a new MPEG-4 audio tag containing the atom.
pub fn with(atoms: Vec<Atom>, readonly_atoms: Vec<Atom>) -> Tag {
Tag { atoms, readonly_atoms }
}
/// Attempts to read a MPEG-4 audio tag from the reader.
pub fn read_from(reader: &mut (impl Read + Seek)) -> crate::Result<Tag> {
atom::read_tag_from(reader)
}
/// Attempts to read a MPEG-4 audio tag from the file at the indicated path.
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
let mut file = BufReader::new(File::open(path)?);
Tag::read_from(&mut file)
}
/// Attempts to write the MPEG-4 audio tag to the writer. This will overwrite any metadata
/// previously present on the file.
pub fn write_to(&self, file: &File) -> crate::Result<()> {
atom::write_tag_to(file, &self.atoms)
}
/// Attempts to write the MPEG-4 audio tag to the path. This will overwrite any metadata
/// previously present on the file.
pub fn write_to_path(&self, path: impl AsRef<Path>) -> crate::Result<()> {
let file = OpenOptions::new().read(true).write(true).open(path)?;
self.write_to(&file)
}
/// Attempts to dump the MPEG-4 audio tag to the writer.
pub fn dump_to(&self, writer: &mut impl Write) -> crate::Result<()> {
atom::dump_tag_to(writer, self.atoms.clone())
}
/// Attempts to dump the MPEG-4 audio tag to the writer.
pub fn dump_to_path(&self, path: impl AsRef<Path>) -> crate::Result<()> {
let mut file = File::create(path)?;
self.dump_to(&mut file)
}
}
// ## Individual string values
mp4ameta_proc::individual_string_value_accessor!("album", "©alb");
mp4ameta_proc::individual_string_value_accessor!("copyright", "cprt");
mp4ameta_proc::individual_string_value_accessor!("encoder", "©too");
mp4ameta_proc::individual_string_value_accessor!("lyrics", "©lyr");
mp4ameta_proc::individual_string_value_accessor!("movement", "©mvn");
mp4ameta_proc::individual_string_value_accessor!("title", "©nam");
mp4ameta_proc::individual_string_value_accessor!("tv_episode_number", "tven");
mp4ameta_proc::individual_string_value_accessor!("tv_network_name", "tvnn");
mp4ameta_proc::individual_string_value_accessor!("tv_show_name", "tvsh");
mp4ameta_proc::individual_string_value_accessor!("work", "©wrk");
mp4ameta_proc::individual_string_value_accessor!("year", "©day");
// ## Multiple string values
mp4ameta_proc::multiple_string_values_accessor!("album_artist", "aART");
mp4ameta_proc::multiple_string_values_accessor!("artist", "©ART");
mp4ameta_proc::multiple_string_values_accessor!("category", "catg");
mp4ameta_proc::multiple_string_values_accessor!("comment", "©cmt");
mp4ameta_proc::multiple_string_values_accessor!("composer", "©wrt");
mp4ameta_proc::multiple_string_values_accessor!("custom_genre", "©gen");
mp4ameta_proc::multiple_string_values_accessor!("description", "desc");
mp4ameta_proc::multiple_string_values_accessor!("grouping", "©grp");
mp4ameta_proc::multiple_string_values_accessor!("keyword", "keyw");
// ## Flags
mp4ameta_proc::flag_value_accessor!("compilation", "cpil");
mp4ameta_proc::flag_value_accessor!("gapless_playback", "pgap");
mp4ameta_proc::flag_value_accessor!("show_movement", "shwm");
mp4ameta_proc::integer_value_accessor!("bpm", "tmpo");
mp4ameta_proc::integer_value_accessor!("movement_count", "©mvc");
mp4ameta_proc::integer_value_accessor!("movement_index", "©mvi");
/// ### Standard genre
impl Tag {
/// Returns all standard genres (`gnre`).
pub fn standard_genres(&self) -> impl Iterator<Item=u16> + '_ {
self.reserved(atom::STANDARD_GENRE)
.filter_map(|v| {
if v.len() < 2 {
None
} else {
Some(u16::from_be_bytes([v[0], v[1]]))
}
})
}
/// Returns the first standard genre (`gnre`).
pub fn standard_genre(&self) -> Option<u16> {
self.standard_genres().next()
}
/// Sets the standard genre (`gnre`). This will remove all other standard genres.
pub fn set_standard_genre(&mut self, genre_code: u16) {
if genre_code > 0 && genre_code <= 80 {
let vec: Vec<u8> = genre_code.to_be_bytes().to_vec();
self.set_data(atom::STANDARD_GENRE, Data::Reserved(vec));
}
}
/// Adds a standard genre (`gnre`).
pub fn add_standard_genre(&mut self, genre_code: u16) {
if genre_code > 0 && genre_code <= 80 {
let vec: Vec<u8> = genre_code.to_be_bytes().to_vec();
self.add_data(atom::STANDARD_GENRE, Data::Reserved(vec))
}
}
/// Removes all standard genres (`gnre`).
pub fn remove_standard_genres(&mut self) {
self.remove_data(atom::STANDARD_GENRE);
}
}
// ## Tuple values
/// ### Track
impl Tag {
/// Returns the track number and the total number of tracks (`trkn`).
pub fn track(&self) -> (Option<u16>, Option<u16>) {
let vec = match self.reserved(atom::TRACK_NUMBER).next() {
Some(v) => v,
None => return (None, None),
};
let track_number = if vec.len() < 4 {
None
} else {
Some(u16::from_be_bytes([vec[2], vec[3]]))
};
let total_tracks = if vec.len() < 6 {
None
} else {
Some(u16::from_be_bytes([vec[4], vec[5]]))
};
(track_number, total_tracks)
}
/// Returns the track number (`trkn`).
pub fn track_number(&self) -> Option<u16> {
let vec = self.reserved(atom::TRACK_NUMBER).next()?;
if vec.len() < 4 {
None
} else {
Some(u16::from_be_bytes([vec[2], vec[3]]))
}
}
/// Returns the total number of tracks (`trkn`).
pub fn total_tracks(&self) -> Option<u16> {
let vec = self.reserved(atom::TRACK_NUMBER).next()?;
if vec.len() < 6 {
None
} else {
Some(u16::from_be_bytes([vec[4], vec[5]]))
}
}
/// Sets the track number and the total number of tracks (`trkn`).
pub fn set_track(&mut self, track_number: u16, total_tracks: u16) {
let vec = vec![0u16, track_number, total_tracks, 0u16].into_iter()
.flat_map(|u| u.to_be_bytes().to_vec())
.collect();
self.set_data(atom::TRACK_NUMBER, Data::Reserved(vec));
}
/// Sets the track number (`trkn`).
pub fn set_track_number(&mut self, track_number: u16) {
if let Some(Data::Reserved(v)) = self.mut_data(atom::TRACK_NUMBER).next() {
if v.len() >= 4 {
let [a, b] = track_number.to_be_bytes();
v[2] = a;
v[3] = b;
return;
}
}
self.set_track(track_number, 0);
}
/// Sets the total number of tracks (`trkn`).
pub fn set_total_tracks(&mut self, total_tracks: u16) {
if let Some(Data::Reserved(v)) = self.mut_data(atom::TRACK_NUMBER).next() {
if v.len() >= 6 {
let [a, b] = total_tracks.to_be_bytes();
v[4] = a;
v[5] = b;
return;
}
}
self.set_track(0, total_tracks);
}
/// Removes the track number and the total number of tracks (`trkn`).
pub fn remove_track(&mut self) {
self.remove_data(atom::TRACK_NUMBER);
}
}
/// ### Disc
impl Tag {
/// Returns the disc number and total number of discs (`disk`).
pub fn disc(&self) -> (Option<u16>, Option<u16>) {
let vec = match self.reserved(atom::DISC_NUMBER).next() {
Some(v) => v,
None => return (None, None),
};
let disc_number = if vec.len() < 4 {
None
} else {
Some(u16::from_be_bytes([vec[2], vec[3]]))
};
let total_discs = if vec.len() < 6 {
None
} else {
Some(u16::from_be_bytes([vec[4], vec[5]]))
};
(disc_number, total_discs)
}
/// Returns the disc number (`disk`).
pub fn disc_number(&self) -> Option<u16> {
let vec = self.reserved(atom::DISC_NUMBER).next()?;
if vec.len() < 4 {
None
} else {
Some(u16::from_be_bytes([vec[2], vec[3]]))
}
}
/// Returns the total number of discs (`disk`).
pub fn total_discs(&self) -> Option<u16> {
let vec = self.reserved(atom::DISC_NUMBER).next()?;
if vec.len() < 6 {
None
} else {
Some(u16::from_be_bytes([vec[4], vec[5]]))
}
}
/// Sets the disc number and the total number of discs (`disk`).
pub fn set_disc(&mut self, disc_number: u16, total_discs: u16) {
let vec = vec![0u16, disc_number, total_discs].into_iter()
.flat_map(|u| u.to_be_bytes().to_vec())
.collect();
self.set_data(atom::DISC_NUMBER, Data::Reserved(vec));
}
/// Sets the disc number (`disk`).
pub fn set_disc_number(&mut self, disc_number: u16) {
if let Some(Data::Reserved(v)) = self.mut_data(atom::DISC_NUMBER).next() {
if v.len() >= 4 {
let [a, b] = disc_number.to_be_bytes();
v[2] = a;
v[3] = b;
return;
}
}
self.set_disc(disc_number, 0);
}
/// Sets the total number of discs (`disk`).
pub fn set_total_discs(&mut self, total_discs: u16) {
if let Some(Data::Reserved(v)) = self.mut_data(atom::DISC_NUMBER).next() {
if v.len() >= 6 {
let [a, b] = total_discs.to_be_bytes();
v[4] = a;
v[5] = b;
return;
}
}
self.set_disc(0, total_discs);
}
/// Removes the disc number and the total number of discs (`disk`).
pub fn remove_disc(&mut self) {
self.remove_data(atom::DISC_NUMBER);
}
}
// ## Custom values
/// ### Artwork
impl Tag {
/// Returns the artwork image data of type [`Data::Jpeg`](enum.Data.html#variant.Jpeg) or
/// [Data::Png](enum.Data.html#variant.Png) (`covr`).
pub fn artworks(&self) -> impl Iterator<Item=&Data> {
self.image(atom::ARTWORK)
}
/// Returns the artwork image data of type [Data::Jpeg](enum.Data.html#variant.Jpeg) or
/// [Data::Png](enum.Data.html#variant.Png) (`covr`).
pub fn artwork(&self) -> Option<&Data> {
self.image(atom::ARTWORK).next()
}
/// Sets the artwork image data of type [Data::Jpeg](enum.Data.html#variant.Jpeg) or
/// [Data::Png](enum.Data.html#variant.Png) (`covr`).
pub fn set_artwork(&mut self, image: Data) {
match &image {
Data::Jpeg(_) => (),
Data::Png(_) => (),
_ => return,
}
self.set_data(atom::ARTWORK, image);
}
/// Adds artwork image data of type [Data::Jpeg](enum.Data.html#variant.Jpeg) or
/// [Data::Png](enum.Data.html#variant.Png) (`covr`). This will remove all other artworks.
pub fn add_artwork(&mut self, image: Data) {
match &image {
Data::Jpeg(_) => (),
Data::Png(_) => (),
_ => return,
}
self.add_data(atom::ARTWORK, image);
}
/// Removes the artwork image data (`covr`).
pub fn remove_artwork(&mut self) {
self.remove_data(atom::ARTWORK);
}
}
/// ### Media type
impl Tag {
/// Returns the media type (`stik`).
pub fn media_type(&self) -> Option<MediaType> {
let vec = match self.data(atom::MEDIA_TYPE).next()? {
Data::Reserved(v) => v,
Data::BeSigned(v) => v,
_ => return None,
};
if vec.is_empty() {
return None;
}
MediaType::try_from(vec[0]).ok()
}
/// Sets the media type (`stik`).
pub fn set_media_type(&mut self, media_type: MediaType) {
self.set_data(atom::MEDIA_TYPE, Data::Reserved(vec![media_type.value()]));
}
/// Removes the media type (`stik`).
pub fn remove_media_type(&mut self) {
self.remove_data(atom::MEDIA_TYPE);
}
}
/// ### Advisory rating
impl Tag {
/// Returns the advisory rating (`rtng`).
pub fn advisory_rating(&self) -> Option<AdvisoryRating> {
let vec = match self.data(atom::ADVISORY_RATING).next()? {
Data::Reserved(v) => v,
Data::BeSigned(v) => v,
_ => return None,
};
if vec.is_empty() {
return None;
}
Some(AdvisoryRating::from(vec[0]))
}
/// Sets the advisory rating (`rtng`).
pub fn set_advisory_rating(&mut self, rating: AdvisoryRating) {
self.set_data(atom::ADVISORY_RATING, Data::Reserved(vec![rating.value()]));
}
/// Removes the advisory rating (`rtng`).
pub fn remove_advisory_rating(&mut self) {
self.remove_data(atom::ADVISORY_RATING);
}
}
/// ### Genre
///
/// These are convenience functions that combine the values from the standard genre (`gnre`) and
/// custom genre (`©gen`).
impl Tag {
/// Returns all genres (gnre or ©gen).
pub fn genres(&self) -> impl Iterator<Item=&str> {
self.standard_genres().filter_map(|genre_code| {
for g in STANDARD_GENRES.iter() {
if g.0 == genre_code {
return Some(g.1);
}
}
None
}).chain(
self.custom_genres()
)
}
/// Returns the first genre (gnre or ©gen).
pub fn genre(&self) -> Option<&str> {
if let Some(genre_code) = self.standard_genre() {
for g in STANDARD_GENRES.iter() {
if g.0 == genre_code {
return Some(g.1);
}
}
}
self.custom_genre()
}
/// Sets the standard genre (`gnre`) if it matches a predefined value otherwise a custom genre
/// (`©gen`). This will remove all other standard or custom genres.
pub fn set_genre(&mut self, genre: impl Into<String>) {
let gen = genre.into();
for g in STANDARD_GENRES.iter() {
if g.1 == gen {
self.remove_custom_genres();
self.set_standard_genre(g.0);
return;
}
}
self.remove_standard_genres();
self.set_custom_genre(gen)
}
/// Adds the standard genre (`gnre`) if it matches one otherwise a custom genre (`©gen`).
pub fn add_genre(&mut self, genre: impl Into<String>) {
let gen = genre.into();
for g in STANDARD_GENRES.iter() {
if g.1 == gen {
self.add_standard_genre(g.0);
return;
}
}
self.add_custom_genre(gen)
}
/// Removes the genre (gnre or ©gen).
pub fn remove_genres(&mut self) {
self.remove_standard_genres();
self.remove_custom_genres();
}
}
// ## Readonly values
/// ### Duration
impl Tag {
/// Returns the duration in seconds.
pub fn duration(&self) -> Option<f64> {
// [Spec](https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-SW34)
let mut vec = None;
for a in &self.readonly_atoms {
if a.ident == atom::MEDIA_HEADER {
if let Content::RawData(Data::Reserved(v)) = &a.content {
vec = Some(v);
break;
}
}
}
let vec = vec?;
if vec.len() < 24 {
return None;
}
let buf: Vec<u32> = vec
.chunks_exact(4)
.map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]]))
.collect();
let timescale_unit = buf[3];
let duration_units = buf[4];
let duration = duration_units as f64 / timescale_unit as f64;
Some(duration)
}
}
/// ### Filetype
impl Tag {
/// returns the filetype (`ftyp`).
pub fn filetype(&self) -> Option<&str> {
for a in &self.readonly_atoms {
if a.ident == atom::FILETYPE {
if let Content::RawData(Data::Utf8(s)) = &a.content {
return Some(s);
}
}
}
None
}
}
/// ## Accessors
impl Tag {
/// Returns all byte data corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Reserved(vec![1,2,3,4,5,6]));
/// assert_eq!(tag.reserved(Ident(*b"test")).next().unwrap().to_vec(), vec![1,2,3,4,5,6]);
/// ```
pub fn reserved(&self, ident: Ident) -> impl Iterator<Item=&Vec<u8>> {
self.data(ident).filter_map(|d| {
match d {
Data::Reserved(v) => Some(v),
_ => None,
}
})
}
/// Returns all byte data representing a big endian integer corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::BeSigned(vec![1,2,3,4,5,6]));
/// assert_eq!(tag.be_signed(Ident(*b"test")).next().unwrap().to_vec(), vec![1,2,3,4,5,6]);
/// ```
pub fn be_signed(&self, ident: Ident) -> impl Iterator<Item=&Vec<u8>> {
self.data(ident).filter_map(|d| {
match d {
Data::BeSigned(v) => Some(v),
_ => None,
}
})
}
/// Returns all string references corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// assert_eq!(tag.string(Ident(*b"test")).next().unwrap(), "data");
/// ```
pub fn string(&self, ident: Ident) -> impl Iterator<Item=&str> {
self.data(ident).filter_map(|d| {
match d {
Data::Utf8(s) => Some(&**s),
Data::Utf16(s) => Some(&**s),
_ => None,
}
})
}
/// Returns all mutable string references corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// tag.mut_string(Ident(*b"test")).next().unwrap().push('1');
/// assert_eq!(tag.string(Ident(*b"test")).next().unwrap(), "data1");
/// ```
pub fn mut_string(&mut self, ident: Ident) -> impl Iterator<Item=&mut String> {
self.mut_data(ident).filter_map(|d| {
match d {
Data::Utf8(s) => Some(s),
Data::Utf16(s) => Some(s),
_ => None,
}
})
}
/// Returns all image data of type [Data::Jpeg](enum.Data.html#variant.Jpeg) or
/// [Data::Jpeg](enum.Data.html#variant.Png) corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Jpeg("<the image data>".as_bytes().to_vec()));
/// match tag.image(Ident(*b"test")).next().unwrap() {
/// Data::Jpeg(v) => assert_eq!(*v, "<the image data>".as_bytes()),
/// _ => panic!("data does not match"),
/// };
/// ```
pub fn image(&self, ident: Ident) -> impl Iterator<Item=&Data> {
self.data(ident).filter(|d| {
match d {
Data::Jpeg(_) => true,
Data::Png(_) => true,
_ => false,
}
})
}
/// Returns all data references corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// match tag.data(Ident(*b"test")).next().unwrap() {
/// Data::Utf8(s) => assert_eq!(s, "data"),
/// _ => panic!("data does not match"),
/// };
/// ```
pub fn data(&self, ident: Ident) -> impl Iterator<Item=&Data> {
self.atoms.iter().filter_map(|a| {
if a.ident == ident {
if let Content::TypedData(d) = &a.first_child()?.content {
return Some(d);
}
}
None
}).collect::<Vec<&Data>>().into_iter()
}
/// Returns all mutable data references corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// if let Data::Utf8(s) = tag.mut_data(Ident(*b"test")).next().unwrap() {
/// s.push('1');
/// }
/// assert_eq!(tag.string(Ident(*b"test")).next().unwrap(), "data1");
/// ```
pub fn mut_data(&mut self, ident: Ident) -> impl Iterator<Item=&mut Data> {
self.atoms.iter_mut().filter_map(|a| {
if a.ident == ident {
if let Content::TypedData(d) = &mut a.mut_first_child()?.content {
return Some(d);
}
}
None
}).collect::<Vec<&mut Data>>().into_iter()
}
/// Removes all other atoms, corresponding to the identifier, and adds a new atom containing the
/// provided data.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// assert_eq!(tag.string(Ident(*b"test")).next().unwrap(), "data");
/// ```
pub fn set_data(&mut self, ident: Ident, data: Data) {
self.remove_data(ident);
self.atoms.push(Atom::with(ident, 0, Content::data_atom_with(data)));
}
/// Adds a new atom, corresponding to the identifier, containing the provided data.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.add_data(Ident(*b"test"), Data::Utf8("data1".into()));
/// tag.add_data(Ident(*b"test"), Data::Utf8("data2".into()));
/// let mut strings = tag.string(Ident(*b"test"));
/// assert_eq!(strings.next().unwrap(), "data1");
/// assert_eq!(strings.next().unwrap(), "data2");
/// ```
pub fn add_data(&mut self, ident: Ident, data: Data) {
self.atoms.push(Atom::with(ident, 0, Content::data_atom_with(data)));
}
/// Removes the data corresponding to the identifier.
///
/// # Example
/// ```
/// use mp4ameta::{Tag, Data, Ident};
///
/// let mut tag = Tag::default();
/// tag.set_data(Ident(*b"test"), Data::Utf8("data".into()));
/// assert!(tag.data(Ident(*b"test")).next().is_some());
/// tag.remove_data(Ident(*b"test"));
/// assert!(tag.data(Ident(*b"test")).next().is_none());
/// ```
pub fn remove_data(&mut self, ident: Ident) {
let mut i = 0;
while i < self.atoms.len() {
if self.atoms[i].ident == ident {
self.atoms.remove(i);
} else {
i += 1;
}
}
}
}

368
src/lib.rs Normal file
View file

@ -0,0 +1,368 @@
//! This crate makes it easier to parse tags/metadata in audio files of different file types.
//!
//! This crate aims to provide a unified trait for parsers and writers of different audio file formats. This means that you can parse tags in mp3 and m4a files with a single function: `audiotags::from_path()` and get fields by directly calling `.album()`, `.artist()` on its result. Without this crate, you would otherwise need to learn different APIs in **id3**, **mp4ameta** crates in order to parse metadata in different file foramts.
//!
//! ## Example
//!
//! ```ignore
//! use audiotags;
//!
//! fn main() {
//! const MP3: &'static str = "a.mp3";
//! let mut tags = audiotags::from_path(MP3).unwrap();
//! // without this crate you would call id3::Tag::from_path()
//! println!("Title: {:?}", tags.title());
//! println!("Artist: {:?}", tags.artist());
//! tags.set_album_artist("CINDERELLA PROJECT");
//! let album = tags.album().unwrap();
//! println!("Album title and artist: {:?}", (album.title, album.artist));
//! println!("Track: {:?}", tags.track());
//! tags.write_to_path(MP3).unwrap();
//! // Title: Some("お願い!シンデレラ")
//! // Artist: Some("高垣楓、城ヶ崎美嘉、小日向美穂、十時愛梨、川島瑞樹、日野茜、輿水幸子、佐久間まゆ、白坂小梅")
//! // Album title and artist: ("THE IDOLM@STER CINDERELLA GIRLS ANIMATION PROJECT 01 Star!!", Some("CINDERELLA PROJECT"))
//! // Track: (Some(2), Some(4))
//!
//! const M4A: &'static str = "b.m4a";
//! let mut tags = audiotags::from_path(M4A).unwrap();
//! // without this crate you would call mp4ameta::Tag::from_path()
//! println!("Title: {:?}", tags.title());
//! println!("Artist: {:?}", tags.artist());
//! let album = tags.album().unwrap();
//! println!("Album title and artist: {:?}", (album.title, album.artist));
//! tags.set_total_tracks(4);
//! println!("Track: {:?}", tags.track());
//! tags.write_to_path(M4A).unwrap();
//! // Title: Some("ふわふわ時間")
//! // Artist: Some("桜高軽音部 [平沢唯・秋山澪・田井中律・琴吹紬(CV:豊崎愛生、日笠陽子、佐藤聡美、寿美菜子)]")
//! // Album title and artist: ("ふわふわ時間", Some("桜高軽音部 [平沢唯・秋山澪・田井中律・琴吹紬(CV:豊崎愛生、日笠陽子、佐藤聡美、寿美菜子)]"))
//! // Track: (Some(1), Some(4))
//! }
//! ```
use id3;
use mp4ameta;
use std::fs::File;
use std::path::Path;
use strum::Display;
type BoxedError = Box<dyn std::error::Error>;
#[derive(Debug, Display)]
pub enum Error {
UnsupportedFormat(String),
}
impl std::error::Error for Error {}
/// Guesses the audio metadata handler from the file extension, and returns the `Box`ed IO handler.
pub fn from_path(path: impl AsRef<Path>) -> Result<Box<dyn AudioTagsIo>, BoxedError> {
match path
.as_ref()
.extension()
.unwrap()
.to_string_lossy()
.to_string()
.to_lowercase()
.as_str()
{
"mp3" => Ok(Box::new(Id3Tags::from_path(path)?)),
"m4a" | "m4b" | "m4p" | "m4v" | "isom" | "mp4" => Ok(Box::new(M4aTags::from_path(path)?)),
p @ _ => Err(Box::new(Error::UnsupportedFormat(p.to_owned()))),
}
}
#[derive(Debug, Clone)]
pub enum Picture {
Png(Vec<u8>),
Jpeg(Vec<u8>),
Tiff(Vec<u8>),
Bmp(Vec<u8>),
Gif(Vec<u8>),
Unknown,
}
impl Picture {
pub fn with_mime(data: Vec<u8>, mime: &str) -> Self {
match mime {
"image/jpeg" => Self::Jpeg(data),
"image/png" => Self::Png(data),
"image/tiff" => Self::Tiff(data),
"image/bmp" => Self::Bmp(data),
"image/gif" => Self::Gif(data),
_ => Self::Unknown,
}
}
}
#[derive(Debug)]
pub struct Album {
pub title: String,
pub artist: Option<String>,
pub cover: Option<Picture>,
}
/// Implementors of this trait are able to read and write audio metadata.
///
/// Constructor methods e.g. `from_file` should be implemented separately.
pub trait AudioTagsIo {
fn title(&self) -> Option<&str>;
fn set_title(&mut self, title: &str);
fn artist(&self) -> Option<&str>;
fn set_artist(&mut self, artist: &str);
fn year(&self) -> Option<i32>;
fn set_year(&mut self, year: i32);
fn album(&self) -> Option<Album>;
fn album_title(&self) -> Option<&str>;
fn album_artist(&self) -> Option<&str>;
fn album_cover(&self) -> Option<Picture>;
fn set_album(&mut self, album: Album);
fn set_album_title(&mut self, v: &str);
fn set_album_artist(&mut self, v: &str);
fn set_album_cover(&mut self, cover: Picture);
fn track(&self) -> (Option<u16>, Option<u16>);
fn set_track(&mut self, track: u16);
fn set_total_tracks(&mut self, total_track: u16);
fn write_to(&self, file: &File) -> Result<(), BoxedError>;
// cannot use impl AsRef<Path>
fn write_to_path(&self, path: &str) -> Result<(), BoxedError>;
}
pub struct Id3Tags {
inner: id3::Tag,
}
impl Id3Tags {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, BoxedError> {
Ok(Self {
inner: id3::Tag::read_from_path(path)?,
})
}
}
impl AudioTagsIo for Id3Tags {
fn title(&self) -> Option<&str> {
self.inner.title()
}
fn set_title(&mut self, title: &str) {
self.inner.set_title(title)
}
fn artist(&self) -> Option<&str> {
self.inner.artist()
}
fn set_artist(&mut self, artist: &str) {
self.inner.set_title(artist)
}
fn year(&self) -> Option<i32> {
self.inner.year()
}
fn set_year(&mut self, year: i32) {
self.inner.set_year(year)
}
fn album(&self) -> Option<Album> {
self.inner.album().map(|title| Album {
title: title.to_owned(),
artist: self.inner.album_artist().map(|x| x.to_owned()),
cover: self.album_cover(),
})
}
fn album_title(&self) -> Option<&str> {
self.inner.album()
}
fn album_artist(&self) -> Option<&str> {
self.inner.album_artist()
}
fn album_cover(&self) -> Option<Picture> {
self.inner
.pictures()
.filter(|&pic| matches!(pic.picture_type, id3::frame::PictureType::CoverFront))
.next()
.map(|pic| Picture::with_mime(pic.data.clone(), &pic.mime_type))
}
fn set_album(&mut self, album: Album) {
self.inner.set_album(album.title);
if let Some(artist) = album.artist {
self.inner.set_album_artist(artist)
} else {
self.inner.remove_album_artist()
}
if let Some(pic) = album.cover {
self.set_album_cover(pic)
} else {
self.inner
.remove_picture_by_type(id3::frame::PictureType::CoverFront);
}
}
fn set_album_title(&mut self, v: &str) {
self.inner.set_album(v)
}
fn set_album_artist(&mut self, v: &str) {
self.inner.set_album_artist(v)
}
fn set_album_cover(&mut self, cover: Picture) {
self.inner
.remove_picture_by_type(id3::frame::PictureType::CoverFront);
self.inner.add_picture(match cover {
Picture::Jpeg(data) => id3::frame::Picture {
mime_type: "jpeg".to_owned(),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: data,
},
Picture::Png(data) => id3::frame::Picture {
mime_type: "png".to_owned(),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: data,
},
Picture::Tiff(data) => id3::frame::Picture {
mime_type: "tiff".to_owned(),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: data,
},
Picture::Bmp(data) => id3::frame::Picture {
mime_type: "bmp".to_owned(),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: data,
},
Picture::Gif(data) => id3::frame::Picture {
mime_type: "gif".to_owned(),
picture_type: id3::frame::PictureType::CoverFront,
description: "".to_owned(),
data: data,
},
_ => panic!("Picture format not supported!"),
});
}
fn track(&self) -> (Option<u16>, Option<u16>) {
(
self.inner.track().map(|x| x as u16),
self.inner.total_tracks().map(|x| x as u16),
)
}
fn set_track(&mut self, track: u16) {
self.inner.set_track(track as u32);
}
fn set_total_tracks(&mut self, total_track: u16) {
self.inner.set_total_tracks(total_track as u32);
}
fn write_to(&self, file: &File) -> Result<(), BoxedError> {
self.inner.write_to(file, id3::Version::Id3v24)?;
Ok(())
}
fn write_to_path(&self, path: &str) -> Result<(), BoxedError> {
self.inner.write_to_path(path, id3::Version::Id3v24)?;
Ok(())
}
}
pub struct M4aTags {
inner: mp4ameta::Tag,
}
impl M4aTags {
pub fn from_path(path: impl AsRef<Path>) -> Result<Self, BoxedError> {
Ok(Self {
inner: mp4ameta::Tag::read_from_path(path)?,
})
}
}
impl AudioTagsIo for M4aTags {
fn title(&self) -> Option<&str> {
self.inner.title()
}
fn set_title(&mut self, title: &str) {
self.inner.set_title(title)
}
fn artist(&self) -> Option<&str> {
self.inner.artist()
}
fn set_artist(&mut self, artist: &str) {
self.inner.set_title(artist)
}
fn year(&self) -> Option<i32> {
match self.inner.year() {
Some(year) => str::parse(year).ok(),
None => None,
}
}
fn set_year(&mut self, year: i32) {
self.inner.set_year(year.to_string())
}
fn album(&self) -> Option<Album> {
self.inner.album().map(|title| Album {
title: title.to_owned(),
artist: self.inner.album_artist().map(|x| x.to_owned()),
cover: self.album_cover(),
})
}
fn album_cover(&self) -> Option<Picture> {
use mp4ameta::Data::*;
self.inner.artwork().map(|data| match data {
Jpeg(d) => Picture::Jpeg(d.clone()),
Png(d) => Picture::Png(d.clone()),
_ => Picture::Unknown,
})
}
fn album_title(&self) -> Option<&str> {
self.inner.album()
}
fn album_artist(&self) -> Option<&str> {
self.inner.album_artist()
}
fn set_album(&mut self, album: Album) {
self.inner.set_album(album.title);
if let Some(artist) = album.artist {
self.inner.set_album_artist(artist)
} else {
// self.inner.remove_album_artist(artist)
}
if let Some(pic) = album.cover {
self.set_album_cover(pic)
} else {
self.inner.remove_artwork();
}
}
fn set_album_cover(&mut self, cover: Picture) {
self.inner.remove_artwork();
self.inner.add_artwork(match cover {
Picture::Png(data) => mp4ameta::Data::Png(data),
Picture::Jpeg(data) => mp4ameta::Data::Jpeg(data),
_ => panic!("Only png and jpeg are supported in m4a"),
});
}
fn set_album_title(&mut self, v: &str) {
self.inner.set_album(v)
}
fn set_album_artist(&mut self, v: &str) {
self.inner.set_album_artist(v)
}
fn track(&self) -> (Option<u16>, Option<u16>) {
self.inner.track()
}
fn set_track(&mut self, track: u16) {
self.inner.set_track_number(track);
}
fn set_total_tracks(&mut self, total_track: u16) {
self.inner.set_total_tracks(total_track);
}
fn write_to(&self, file: &File) -> Result<(), BoxedError> {
self.inner.write_to(file)?;
Ok(())
}
fn write_to_path(&self, path: &str) -> Result<(), BoxedError> {
self.inner.write_to_path(path)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}