mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
0.0.1
This commit is contained in:
parent
c43090f291
commit
57171bd561
19 changed files with 3247 additions and 0 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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
15
Cargo.toml
Normal 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
43
README.md
Normal 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
19
rust-mp4ameta/Cargo.toml
Normal 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"
|
201
rust-mp4ameta/LICENSE-APACHE
Normal file
201
rust-mp4ameta/LICENSE-APACHE
Normal 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
21
rust-mp4ameta/LICENSE-MIT
Normal 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
46
rust-mp4ameta/README.md
Normal 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
|
||||
```
|
||||
|
2
rust-mp4ameta/mp4ameta_proc/.gitignore
vendored
Normal file
2
rust-mp4ameta/mp4ameta_proc/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
Cargo.lock
|
11
rust-mp4ameta/mp4ameta_proc/Cargo.toml
Normal file
11
rust-mp4ameta/mp4ameta_proc/Cargo.toml
Normal 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
|
209
rust-mp4ameta/mp4ameta_proc/src/lib.rs
Normal file
209
rust-mp4ameta/mp4ameta_proc/src/lib.rs
Normal 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:")
|
||||
}
|
736
rust-mp4ameta/src/core/atom.rs
Normal file
736
rust-mp4ameta/src/core/atom.rs
Normal 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()),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
208
rust-mp4ameta/src/core/content.rs
Normal file
208
rust-mp4ameta/src/core/content.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
248
rust-mp4ameta/src/core/data.rs
Normal file
248
rust-mp4ameta/src/core/data.rs
Normal 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)?)
|
||||
}
|
7
rust-mp4ameta/src/core/mod.rs
Normal file
7
rust-mp4ameta/src/core/mod.rs
Normal 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;
|
118
rust-mp4ameta/src/core/types.rs
Normal file
118
rust-mp4ameta/src/core/types.rs
Normal 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
106
rust-mp4ameta/src/error.rs
Normal 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
45
rust-mp4ameta/src/lib.rs
Normal 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
839
rust-mp4ameta/src/tag.rs
Normal 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
368
src/lib.rs
Normal 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);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue