mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-12-12 21:52:33 +00:00
use mp4ameta = 0.5.1
This commit is contained in:
parent
57171bd561
commit
d9fd42f491
16 changed files with 1 additions and 2817 deletions
|
@ -11,5 +11,5 @@ repository = "https://github.com/TianyiShi2001/audiotags"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
id3 = "0.5.1"
|
id3 = "0.5.1"
|
||||||
mp4ameta = {path = "./rust-mp4ameta"}
|
mp4ameta = "0.5.1"
|
||||||
strum = {version = "0.19.5", features = ["derive"]}
|
strum = {version = "0.19.5", features = ["derive"]}
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
[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"
|
|
|
@ -1,201 +0,0 @@
|
||||||
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.
|
|
|
@ -1,21 +0,0 @@
|
||||||
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.
|
|
|
@ -1,46 +0,0 @@
|
||||||
# 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
2
rust-mp4ameta/mp4ameta_proc/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
/target
|
|
||||||
Cargo.lock
|
|
|
@ -1,11 +0,0 @@
|
||||||
[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
|
|
|
@ -1,209 +0,0 @@
|
||||||
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:")
|
|
||||||
}
|
|
|
@ -1,736 +0,0 @@
|
||||||
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()),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,208 +0,0 @@
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,248 +0,0 @@
|
||||||
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)?)
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
/// 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;
|
|
|
@ -1,118 +0,0 @@
|
||||||
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,106 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
//! 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;
|
|
|
@ -1,839 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue