mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Implement custom file resolvers (#40)
This commit is contained in:
parent
85ad435898
commit
f48014fda8
30 changed files with 995 additions and 663 deletions
|
@ -6,6 +6,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **A new file resolver system**:
|
||||
- New module: `lofty::resolve`
|
||||
- With this, you will be able to create your own `FileType`s, while continuing
|
||||
to use lofty's traditional API. Read the module docs for more info.
|
||||
- **A proc macro for file creation**:
|
||||
- With the new `lofty_attr` crate, file creation has been simplified significantly.
|
||||
It is available for both internal and external usage.
|
||||
|
||||
## [0.7.3] - 2022-07-22
|
||||
|
||||
### Added
|
||||
|
|
11
Cargo.toml
11
Cargo.toml
|
@ -19,6 +19,7 @@ byteorder = "1.4.3"
|
|||
cfg-if = "1.0.0"
|
||||
# ID3 compressed frames
|
||||
flate2 = { version = "1.0.24", optional = true }
|
||||
lofty_attr = { path = "lofty_attr" }
|
||||
# OGG Vorbis/Opus
|
||||
ogg_pager = "0.3.2"
|
||||
# Key maps
|
||||
|
@ -38,11 +39,11 @@ riff_info_list = []
|
|||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.3.6", features = ["html_reports"] }
|
||||
tempfile = "3.3.0"
|
||||
# tag_writer example
|
||||
structopt = { version = "0.3.26", default-features = false }
|
||||
# WAV properties validity tests
|
||||
hound = { git = "https://github.com/ruuda/hound.git", rev = "02e66effb33683dd6acb92df792683ee46ad6a59" }
|
||||
# tag_writer example
|
||||
structopt = { version = "0.3.26", default-features = false }
|
||||
tempfile = "3.3.0"
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
@ -55,6 +56,10 @@ harness = false
|
|||
name = "create_tag"
|
||||
harness = false
|
||||
|
||||
[[example]]
|
||||
name = "custom_resolver"
|
||||
path = "examples/custom_resolver/src/main.rs"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
|
@ -28,6 +28,7 @@ Parse, convert, and write metadata to various audio formats.
|
|||
* [Tag reader](examples/tag_reader.rs)
|
||||
* [Tag stripper](examples/tag_stripper.rs)
|
||||
* [Tag writer](examples/tag_writer.rs)
|
||||
* [Custom resolver](examples/custom_resolver)
|
||||
|
||||
To try them out, run:
|
||||
|
||||
|
@ -35,6 +36,7 @@ To try them out, run:
|
|||
cargo run --example tag_reader /path/to/file
|
||||
cargo run --example tag_stripper /path/to/file
|
||||
cargo run --example tag_writer <options> /path/to/file
|
||||
cargo run --example custom_resolver
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
|
10
examples/custom_resolver/Cargo.toml
Normal file
10
examples/custom_resolver/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "custom_resolver"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
lofty = { path = "../.." }
|
||||
lofty_attr = { path = "../../lofty_attr" }
|
106
examples/custom_resolver/src/main.rs
Normal file
106
examples/custom_resolver/src/main.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use lofty::ape::ApeTag;
|
||||
use lofty::error::Result as LoftyResult;
|
||||
use lofty::id3::v2::ID3v2Tag;
|
||||
use lofty::resolve::FileResolver;
|
||||
use lofty::{FileProperties, FileType, TagType};
|
||||
use lofty_attr::LoftyFile;
|
||||
use std::fs::File;
|
||||
|
||||
#[rustfmt::skip]
|
||||
// This `LoftyFile` derive will setup most of the necessary boilerplate
|
||||
// for you.
|
||||
#[derive(LoftyFile)]
|
||||
// `read_fn` is the function that will house your parsing logic.
|
||||
// See `lofty::AudioFile::read_from` for the expected signature.
|
||||
#[lofty(read_fn = "Self::parse_my_file")]
|
||||
// The `FileType` variant of the file
|
||||
#[lofty(file_type = "Custom(\"MyFile\")")]
|
||||
struct MyFile {
|
||||
// A file has two requirements, at least one tag field, and a properties field.
|
||||
|
||||
// Tag field requirements:
|
||||
// * Fields *must* end with "_tag" to set them apart from the others.
|
||||
// * The type of the field *must* implement `Into<Tag>`
|
||||
|
||||
|
||||
// Specify a tag type
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
// Let's say our file *always* has an ID3v2Tag present,
|
||||
// we can indicate that with this.
|
||||
#[lofty(always_present)]
|
||||
pub id3v2_tag: ID3v2Tag,
|
||||
|
||||
// Our APE tag is optional in this format, so we wrap it in an `Option`
|
||||
#[lofty(tag_type = "APE")]
|
||||
pub ape_tag: Option<ApeTag>,
|
||||
|
||||
// The properties field *must* be present and named as such.
|
||||
// The only requirement for this field is that the type *must* implement `Into<FileProperties>`.
|
||||
pub properties: FileProperties,
|
||||
}
|
||||
|
||||
impl MyFile {
|
||||
pub fn parse_my_file<R>(_reader: &mut R, _read_properties: bool) -> LoftyResult<Self>
|
||||
where
|
||||
R: std::io::Read + std::io::Seek,
|
||||
{
|
||||
// Your parsing logic...
|
||||
|
||||
Ok(Self {
|
||||
id3v2_tag: ID3v2Tag::default(),
|
||||
ape_tag: None,
|
||||
properties: FileProperties::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Now, we can setup a resolver for our new file
|
||||
impl FileResolver for MyFile {
|
||||
// The extension of the file, if it has one
|
||||
fn extension() -> Option<&'static str> {
|
||||
Some("myfile")
|
||||
}
|
||||
|
||||
// The primary `TagType` of the file, or the one most
|
||||
// likely to be used with it
|
||||
fn primary_tag_type() -> TagType {
|
||||
TagType::ID3v2
|
||||
}
|
||||
|
||||
// All of the `TagType`s this file supports, including the
|
||||
// primary one.
|
||||
fn supported_tag_types() -> &'static [TagType] {
|
||||
&[TagType::ID3v2, TagType::APE]
|
||||
}
|
||||
|
||||
// This is used to guess the `FileType` when reading the file contents.
|
||||
// We are given the first (up to) 50 bytes to work with.
|
||||
fn guess(buf: &[u8]) -> Option<FileType> {
|
||||
if buf.starts_with(b"myfiledata") {
|
||||
Some(FileType::Custom("MyFile"))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// Now that we've setup our file, we can register it.
|
||||
//
|
||||
// `register_custom_resolver` takes the type of our file, alongside a name.
|
||||
// The name will be used in the `FileType` variant (e.g. FileType::Custom("MyFile")).
|
||||
// The name should preferably match the name of the file struct to avoid confusion.
|
||||
lofty::resolve::register_custom_resolver::<MyFile>("MyFile");
|
||||
|
||||
// Now when using the following functions, your custom file will be checked
|
||||
|
||||
let path = "examples/custom_resolver/test_asset.myfile";
|
||||
|
||||
// Detected from the "myfile" extension
|
||||
let _ = lofty::read_from_path(path, true).unwrap();
|
||||
|
||||
let mut file = File::open(path).unwrap();
|
||||
|
||||
// The file's content starts with "myfiledata"
|
||||
let _ = lofty::read_from(&mut file, true).unwrap();
|
||||
}
|
1
examples/custom_resolver/test_asset.myfile
Normal file
1
examples/custom_resolver/test_asset.myfile
Normal file
|
@ -0,0 +1 @@
|
|||
myfiledata
|
12
lofty_attr/Cargo.toml
Normal file
12
lofty_attr/Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
|||
[package]
|
||||
name = "lofty_attr"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "1.0.95", features = ["full"] }
|
||||
quote = "1.0.18"
|
||||
proc-macro2 = "1.0.39"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
449
lofty_attr/src/lib.rs
Normal file
449
lofty_attr/src/lib.rs
Normal file
|
@ -0,0 +1,449 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Span;
|
||||
use quote::{format_ident, quote, quote_spanned, ToTokens};
|
||||
use std::fmt::Display;
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{
|
||||
parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, Lit, Meta,
|
||||
MetaList, NestedMeta, Type,
|
||||
};
|
||||
|
||||
const LOFTY_FILE_TYPES: [&str; 10] = [
|
||||
"AIFF", "APE", "FLAC", "MP3", "MP4", "Opus", "Vorbis", "Speex", "WAV", "WavPack",
|
||||
];
|
||||
|
||||
#[proc_macro_derive(LoftyFile, attributes(lofty))]
|
||||
pub fn tag(input: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
let mut errors = Vec::new();
|
||||
let ret = parse(input, &mut errors);
|
||||
|
||||
let compile_errors = errors.iter().map(syn::Error::to_compile_error);
|
||||
|
||||
TokenStream::from(quote! {
|
||||
#(#compile_errors)*
|
||||
#ret
|
||||
})
|
||||
}
|
||||
|
||||
fn parse(input: DeriveInput, errors: &mut Vec<syn::Error>) -> proc_macro2::TokenStream {
|
||||
macro_rules! bail {
|
||||
($errors:ident, $span:expr, $msg:literal) => {
|
||||
$errors.push(err($span, $msg));
|
||||
return proc_macro2::TokenStream::new();
|
||||
};
|
||||
}
|
||||
|
||||
let data = match input.data {
|
||||
Data::Struct(
|
||||
ref data_struct @ DataStruct {
|
||||
fields: Fields::Named(_),
|
||||
..
|
||||
},
|
||||
) => data_struct,
|
||||
_ => {
|
||||
bail!(
|
||||
errors,
|
||||
input.ident.span(),
|
||||
"This macro can only be used on structs with named fields"
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
let impl_audiofile = should_impl_audiofile(&input.attrs);
|
||||
|
||||
let read_fn = match get_attr("read_fn", &input.attrs) {
|
||||
Some(rfn) => rfn,
|
||||
_ if impl_audiofile => {
|
||||
bail!(
|
||||
errors,
|
||||
input.ident.span(),
|
||||
"Expected a #[read_fn] attribute"
|
||||
);
|
||||
},
|
||||
_ => proc_macro2::TokenStream::new(),
|
||||
};
|
||||
|
||||
let struct_name = input.ident.clone();
|
||||
|
||||
let file_type = match opt_file_type(struct_name.to_string()) {
|
||||
Some(ft) => ft,
|
||||
_ => match get_attr("file_type", &input.attrs) {
|
||||
Some(rfn) => rfn,
|
||||
_ => {
|
||||
bail!(
|
||||
errors,
|
||||
input.ident.span(),
|
||||
"Expected a #[file_type] attribute"
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let (tag_fields, properties_field) = match get_fields(errors, data) {
|
||||
Some(fields) => fields,
|
||||
None => return proc_macro2::TokenStream::new(),
|
||||
};
|
||||
|
||||
if tag_fields.is_empty() {
|
||||
errors.push(err(input.ident.span(), "Struct has no tag fields"));
|
||||
}
|
||||
|
||||
let properties_field = if let Some(field) = properties_field {
|
||||
field
|
||||
} else {
|
||||
bail!(errors, input.ident.span(), "Struct has no properties field");
|
||||
};
|
||||
|
||||
let properties_field_ty = &properties_field.ty;
|
||||
let assert_properties_impl = quote_spanned! {properties_field_ty.span()=>
|
||||
struct _AssertIntoFileProperties where #properties_field_ty: std::convert::Into<lofty::FileProperties>;
|
||||
};
|
||||
|
||||
let assert_tag_impl_into = tag_fields.iter().enumerate().map(|(i, f)| {
|
||||
let name = format_ident!("_AssertTagExt{}", i);
|
||||
let field_ty = &f.ty;
|
||||
quote_spanned! {field_ty.span()=>
|
||||
struct #name where #field_ty: lofty::TagExt;
|
||||
}
|
||||
});
|
||||
|
||||
let tag_exists = tag_fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
if f.needs_option {
|
||||
quote! { self.#name.is_some() }
|
||||
} else {
|
||||
quote! { true }
|
||||
}
|
||||
});
|
||||
let tag_exists_2 = tag_exists.clone();
|
||||
|
||||
let tag_type = tag_fields.iter().map(|f| &f.tag_type);
|
||||
|
||||
let audiofile_impl = if impl_audiofile {
|
||||
quote! {
|
||||
impl lofty::AudioFile for #struct_name {
|
||||
type Properties = #properties_field_ty;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> lofty::error::Result<Self>
|
||||
where
|
||||
R: std::io::Read + std::io::Seek,
|
||||
{
|
||||
#read_fn(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#( #tag_exists )||*
|
||||
}
|
||||
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#( TagType::#tag_type => { #tag_exists_2 } ),*
|
||||
_ => false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
proc_macro2::TokenStream::new()
|
||||
};
|
||||
|
||||
let conditions = tag_fields.iter().map(|f| {
|
||||
let name = &f.name;
|
||||
if f.needs_option {
|
||||
quote! { if let Some(t) = input.#name { tags.push(t.into()); } }
|
||||
} else {
|
||||
quote! { tags.push(input.#name.into()); }
|
||||
}
|
||||
});
|
||||
|
||||
let getters = get_getters(&tag_fields, &struct_name);
|
||||
|
||||
quote! {
|
||||
#assert_properties_impl
|
||||
|
||||
#( #assert_tag_impl_into )*
|
||||
|
||||
#audiofile_impl
|
||||
|
||||
impl std::convert::From<#struct_name> for lofty::TaggedFile {
|
||||
fn from(input: #struct_name) -> Self {
|
||||
lofty::TaggedFile::new(
|
||||
lofty::FileType::#file_type,
|
||||
lofty::FileProperties::from(input.properties),
|
||||
{
|
||||
let mut tags: Vec<lofty::Tag> = Vec::new();
|
||||
#( #conditions )*
|
||||
|
||||
tags
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#( #getters )*
|
||||
}
|
||||
}
|
||||
|
||||
struct FieldContents {
|
||||
name: Ident,
|
||||
cfg_features: Vec<Attribute>,
|
||||
needs_option: bool,
|
||||
getter_name: Option<proc_macro2::TokenStream>,
|
||||
ty: Type,
|
||||
tag_type: proc_macro2::TokenStream,
|
||||
}
|
||||
|
||||
fn get_fields<'a>(
|
||||
errors: &mut Vec<syn::Error>,
|
||||
data: &'a DataStruct,
|
||||
) -> Option<(Vec<FieldContents>, Option<&'a syn::Field>)> {
|
||||
let mut tag_fields = Vec::new();
|
||||
let mut properties_field = None;
|
||||
|
||||
for field in &data.fields {
|
||||
let name = field.ident.clone().unwrap();
|
||||
if name.to_string().ends_with("_tag") {
|
||||
let tag_type = match get_attr("tag_type", &field.attrs) {
|
||||
Some(tt) => tt,
|
||||
_ => {
|
||||
errors.push(err(field.span(), "Field has no `tag_type` attribute"));
|
||||
return None;
|
||||
},
|
||||
};
|
||||
|
||||
let cfg = field
|
||||
.attrs
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|a| get_attr_list("cfg", &a).map(|_| a))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let contents = FieldContents {
|
||||
name,
|
||||
getter_name: get_attr("getter", &field.attrs),
|
||||
ty: extract_type_from_option(&field.ty)
|
||||
.map_or_else(|| field.ty.clone(), |t| t.clone()),
|
||||
tag_type,
|
||||
needs_option: needs_option(&field.attrs),
|
||||
cfg_features: cfg,
|
||||
};
|
||||
tag_fields.push(contents);
|
||||
continue;
|
||||
}
|
||||
|
||||
if name == "properties" {
|
||||
properties_field = Some(field);
|
||||
}
|
||||
}
|
||||
|
||||
Some((tag_fields, properties_field))
|
||||
}
|
||||
|
||||
fn opt_file_type(struct_name: String) -> Option<proc_macro2::TokenStream> {
|
||||
let stripped = struct_name.strip_suffix("File");
|
||||
if let Some(prefix) = stripped {
|
||||
if let Some(pos) = LOFTY_FILE_TYPES
|
||||
.iter()
|
||||
.position(|p| p.eq_ignore_ascii_case(prefix))
|
||||
{
|
||||
return Some(
|
||||
LOFTY_FILE_TYPES[pos]
|
||||
.parse::<proc_macro2::TokenStream>()
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_attr(name: &str, attrs: &[Attribute]) -> Option<proc_macro2::TokenStream> {
|
||||
for attr in attrs {
|
||||
if let Some(list) = get_attr_list("lofty", attr) {
|
||||
if let Some(NestedMeta::Meta(Meta::NameValue(mnv))) = list.nested.first() {
|
||||
if mnv
|
||||
.path
|
||||
.segments
|
||||
.first()
|
||||
.expect("path shouldn't be empty")
|
||||
.ident == name
|
||||
{
|
||||
if let Lit::Str(lit_str) = &mnv.lit {
|
||||
return Some(lit_str.parse::<proc_macro2::TokenStream>().unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn needs_option(attrs: &[Attribute]) -> bool {
|
||||
for attr in attrs {
|
||||
if has_path_attr(attr, "always_present") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn should_impl_audiofile(attrs: &[Attribute]) -> bool {
|
||||
for attr in attrs {
|
||||
if has_path_attr(attr, "no_audiofile_impl") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn has_path_attr(attr: &Attribute, name: &str) -> bool {
|
||||
if let Some(list) = get_attr_list("lofty", attr) {
|
||||
if let Some(NestedMeta::Meta(Meta::Path(p))) = list.nested.first() {
|
||||
if p.is_ident(name) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn get_attr_list(path: &str, attr: &Attribute) -> Option<MetaList> {
|
||||
if attr.path.is_ident(path) {
|
||||
if let Ok(Meta::List(list)) = attr.parse_meta() {
|
||||
return Some(list);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn get_getters<'a>(
|
||||
tag_fields: &'a [FieldContents],
|
||||
struct_name: &'a Ident,
|
||||
) -> impl Iterator<Item = proc_macro2::TokenStream> + 'a {
|
||||
tag_fields.iter().map(move |f| {
|
||||
let name = f.getter_name.clone().unwrap_or_else(|| {
|
||||
let name = f.name.to_string().strip_suffix("_tag").unwrap().to_string();
|
||||
|
||||
Ident::new(&name, f.name.span()).into_token_stream()
|
||||
});
|
||||
|
||||
let (ty_prefix, ty_suffix) = if f.needs_option {
|
||||
(quote! {Option<}, quote! {>})
|
||||
} else {
|
||||
(quote! {}, quote! {})
|
||||
};
|
||||
|
||||
let field_name = &f.name;
|
||||
let field_ty = &f.ty;
|
||||
|
||||
let assert_field_ty_default = quote_spanned! {f.name.span()=>
|
||||
struct _AssertDefault where #field_ty: core::default::Default;
|
||||
};
|
||||
|
||||
let ref_access = if f.needs_option {
|
||||
quote! {self.#field_name.as_ref()}
|
||||
} else {
|
||||
quote! {&self.#field_name}
|
||||
};
|
||||
|
||||
let mut_ident = Ident::new(&format!("{}_mut", name), Span::call_site());
|
||||
|
||||
let mut_access = if f.needs_option {
|
||||
quote! {self.#field_name.as_mut()}
|
||||
} else {
|
||||
quote! {&mut self.#field_name}
|
||||
};
|
||||
|
||||
let remove_ident = Ident::new(&format!("remove_{}", name), Span::call_site());
|
||||
|
||||
let remover = if f.needs_option {
|
||||
quote! {self.#field_name = None;}
|
||||
} else {
|
||||
quote! {
|
||||
#assert_field_ty_default
|
||||
self.#field_name = <#field_ty>::default();
|
||||
}
|
||||
};
|
||||
|
||||
let cfg = &f.cfg_features;
|
||||
quote! {
|
||||
#( #cfg )*
|
||||
impl #struct_name {
|
||||
/// Returns a reference to the tag
|
||||
pub fn #name(&self) -> #ty_prefix &#field_ty #ty_suffix {
|
||||
#ref_access
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the tag
|
||||
pub fn #mut_ident(&mut self) -> #ty_prefix &mut #field_ty #ty_suffix {
|
||||
#mut_access
|
||||
}
|
||||
|
||||
/// Removes the tag
|
||||
pub fn #remove_ident(&mut self) {
|
||||
#remover
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/55271857/how-can-i-get-the-t-from-an-optiont-when-using-syn
|
||||
fn extract_type_from_option(ty: &Type) -> Option<&Type> {
|
||||
use syn::{GenericArgument, Path, PathArguments, PathSegment};
|
||||
|
||||
fn extract_type_path(ty: &Type) -> Option<&Path> {
|
||||
match *ty {
|
||||
Type::Path(ref typepath) if typepath.qself.is_none() => Some(&typepath.path),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_option_segment(path: &Path) -> Option<&PathSegment> {
|
||||
let idents_of_path = path
|
||||
.segments
|
||||
.iter()
|
||||
.into_iter()
|
||||
.fold(String::new(), |mut acc, v| {
|
||||
acc.push_str(&v.ident.to_string());
|
||||
acc.push('|');
|
||||
acc
|
||||
});
|
||||
vec!["Option|", "std|option|Option|", "core|option|Option|"]
|
||||
.into_iter()
|
||||
.find(|s| idents_of_path == *s)
|
||||
.and_then(|_| path.segments.last())
|
||||
}
|
||||
|
||||
extract_type_path(ty)
|
||||
.and_then(extract_option_segment)
|
||||
.and_then(|path_seg| {
|
||||
let type_params = &path_seg.arguments;
|
||||
// It should have only on angle-bracketed param ("<String>"):
|
||||
match *type_params {
|
||||
PathArguments::AngleBracketed(ref params) => params.args.first(),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.and_then(|generic_arg| match *generic_arg {
|
||||
GenericArgument::Type(ref ty) => Some(ty),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn err<T: Display>(span: Span, error: T) -> syn::Error {
|
||||
syn::Error::new(span, error)
|
||||
}
|
|
@ -11,16 +11,13 @@ mod properties;
|
|||
mod read;
|
||||
pub(crate) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v1")]
|
||||
use crate::id3::v1::tag::ID3v1Tag;
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::tag::ID3v2Tag;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
// Exports
|
||||
|
||||
|
@ -37,89 +34,21 @@ cfg_if::cfg_if! {
|
|||
pub use properties::ApeProperties;
|
||||
|
||||
/// An APE file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct ApeFile {
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// An ID3v1 tag
|
||||
#[cfg(feature = "id3v1")]
|
||||
#[lofty(tag_type = "ID3v1")]
|
||||
pub(crate) id3v1_tag: Option<ID3v1Tag>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag (Not officially supported)
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
pub(crate) id3v2_tag: Option<ID3v2Tag>,
|
||||
#[cfg(feature = "ape")]
|
||||
/// An APEv1/v2 tag
|
||||
#[cfg(feature = "ape")]
|
||||
#[lofty(tag_type = "APE")]
|
||||
pub(crate) ape_tag: Option<ApeTag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: ApeProperties,
|
||||
}
|
||||
|
||||
impl From<ApeFile> for TaggedFile {
|
||||
#[allow(clippy::vec_init_then_push, unused_mut)]
|
||||
fn from(input: ApeFile) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
tags.push(input.ape_tag.map(Into::into));
|
||||
#[cfg(feature = "id3v1")]
|
||||
tags.push(input.id3v1_tag.map(Into::into));
|
||||
#[cfg(feature = "id3v2")]
|
||||
tags.push(input.id3v2_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::APE,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for ApeFile {
|
||||
type Properties = ApeProperties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
Self: Sized,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "ape")]
|
||||
return self.ape_tag.is_some();
|
||||
#[cfg(feature = "id3v1")]
|
||||
return self.id3v1_tag.is_some();
|
||||
#[cfg(feature = "id3v2")]
|
||||
return self.id3v2_tag.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::APE => self.ape_tag.is_some(),
|
||||
#[cfg(feature = "id3v1")]
|
||||
TagType::ID3v1 => self.id3v1_tag.is_some(),
|
||||
#[cfg(feature = "id3v2")]
|
||||
TagType::ID3v2 => self.id3v2_tag.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApeFile {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag, ID3v2Tag;
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
id3v1_tag, ID3v1Tag;
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag, ApeTag
|
||||
}
|
||||
}
|
||||
|
|
53
src/file.rs
53
src/file.rs
|
@ -3,6 +3,7 @@ use crate::properties::FileProperties;
|
|||
use crate::tag::{Tag, TagType};
|
||||
use crate::traits::TagExt;
|
||||
|
||||
use crate::resolve::CUSTOM_RESOLVERS;
|
||||
use std::convert::TryInto;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{File, OpenOptions};
|
||||
|
@ -46,6 +47,16 @@ pub struct TaggedFile {
|
|||
}
|
||||
|
||||
impl TaggedFile {
|
||||
#[doc(hidden)]
|
||||
/// This exists for use in `lofty_attr`, there's no real use for this externally
|
||||
pub fn new(ty: FileType, properties: FileProperties, tags: Vec<Tag>) -> Self {
|
||||
Self {
|
||||
ty,
|
||||
properties,
|
||||
tags,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the file's [`FileType`]
|
||||
///
|
||||
/// # Examples
|
||||
|
@ -480,6 +491,7 @@ pub enum FileType {
|
|||
Speex,
|
||||
WAV,
|
||||
WavPack,
|
||||
Custom(&'static str),
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
|
@ -493,6 +505,10 @@ impl FileType {
|
|||
/// | `FLAC`, `Opus`, `Vorbis` | `VorbisComments` |
|
||||
/// | `MP4` | `Mp4Ilst` |
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`crate::resolve::register_custom_resolver`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
|
@ -519,11 +535,25 @@ impl FileType {
|
|||
TagType::VorbisComments
|
||||
},
|
||||
FileType::MP4 => TagType::MP4ilst,
|
||||
FileType::Custom(c) => {
|
||||
if let Some(r) = crate::resolve::lookup_resolver(c) {
|
||||
r.primary_tag_type()
|
||||
} else {
|
||||
panic!(
|
||||
"Encountered an unregistered custom `FileType` named `{}`",
|
||||
c
|
||||
);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns if the target `FileType` supports a [`TagType`]
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`crate::resolve::register_custom_resolver`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
|
@ -554,6 +584,16 @@ impl FileType {
|
|||
FileType::MP4 => tag_type == TagType::MP4ilst,
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
FileType::WAV => tag_type == TagType::RIFFInfo,
|
||||
FileType::Custom(c) => {
|
||||
if let Some(r) = crate::resolve::lookup_resolver(c) {
|
||||
r.supported_tag_types().contains(&tag_type)
|
||||
} else {
|
||||
panic!(
|
||||
"Encountered an unregistered custom `FileType` named `{}`",
|
||||
c
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -585,7 +625,18 @@ impl FileType {
|
|||
"ogg" => Some(Self::Vorbis),
|
||||
"mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::MP4),
|
||||
"spx" => Some(Self::Speex),
|
||||
_ => None,
|
||||
e => {
|
||||
if let Some((ty, _)) = CUSTOM_RESOLVERS
|
||||
.lock()
|
||||
.ok()?
|
||||
.iter()
|
||||
.find(|(_, f)| f.extension() == Some(e))
|
||||
{
|
||||
Some(Self::Custom(ty))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,16 +10,14 @@ mod read;
|
|||
#[cfg(feature = "vorbis_comments")]
|
||||
pub(crate) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::tag::ID3v2Tag;
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
use crate::ogg::VorbisComments;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
/// A FLAC file
|
||||
///
|
||||
|
@ -30,78 +28,19 @@ use std::io::{Read, Seek};
|
|||
/// comments block, but `FlacFile::vorbis_comments` will exist.
|
||||
/// * When writing, the pictures will be stored in their own picture blocks
|
||||
/// * This behavior will likely change in the future
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct FlacFile {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
pub(crate) id3v2_tag: Option<ID3v2Tag>,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: This field being `Some` does not mean the file has vorbis comments, as Picture blocks exist.
|
||||
pub(crate) vorbis_comments: Option<VorbisComments>,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
#[lofty(tag_type = "VorbisComments")]
|
||||
pub(crate) vorbis_comments_tag: Option<VorbisComments>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: FileProperties,
|
||||
}
|
||||
|
||||
impl From<FlacFile> for TaggedFile {
|
||||
#[allow(clippy::vec_init_then_push)]
|
||||
fn from(input: FlacFile) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(2);
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
tags.push(input.vorbis_comments.map(Into::into));
|
||||
#[cfg(feature = "id3v2")]
|
||||
tags.push(input.id3v2_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::FLAC,
|
||||
properties: input.properties,
|
||||
#[cfg(any(feature = "vorbis_comments", feature = "id3v2"))]
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
#[cfg(not(any(feature = "vorbis_comments", feature = "id3v2")))]
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for FlacFile {
|
||||
type Properties = FileProperties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
return self.vorbis_comments.is_some();
|
||||
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
return false;
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
return tag_type == TagType::VorbisComments && self.vorbis_comments.is_some();
|
||||
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
impl FlacFile {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
vorbis_comments, VorbisComments;
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag, ID3v2Tag
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ where
|
|||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag: None,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
vorbis_comments: None,
|
||||
vorbis_comments_tag: None,
|
||||
properties: FileProperties::default(),
|
||||
};
|
||||
|
||||
|
@ -108,7 +108,7 @@ where
|
|||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
{
|
||||
flac_file.vorbis_comments =
|
||||
flac_file.vorbis_comments_tag =
|
||||
(!(tag.items.is_empty() && tag.pictures.is_empty())).then(|| tag);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,12 @@ mod properties;
|
|||
mod read;
|
||||
pub(crate) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::tag::ID3v2Tag;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "aiff_text_chunks")] {
|
||||
|
@ -19,77 +17,17 @@ cfg_if::cfg_if! {
|
|||
}
|
||||
|
||||
/// An AIFF file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct AiffFile {
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
/// Any text chunks included in the file
|
||||
pub(crate) text_chunks: Option<AIFFTextChunks>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
#[lofty(tag_type = "AIFFText")]
|
||||
pub(crate) text_chunks_tag: Option<AIFFTextChunks>,
|
||||
/// An ID3v2 tag
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
pub(crate) id3v2_tag: Option<ID3v2Tag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: FileProperties,
|
||||
}
|
||||
|
||||
impl From<AiffFile> for TaggedFile {
|
||||
#[allow(unused_mut)]
|
||||
fn from(input: AiffFile) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
|
||||
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
tags.push(input.text_chunks.map(Into::into));
|
||||
#[cfg(feature = "id3v2")]
|
||||
tags.push(input.id3v2_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::AIFF,
|
||||
properties: input.properties,
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for AiffFile {
|
||||
type Properties = FileProperties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
Self: Sized,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "id3v2")]
|
||||
return self.id3v2_tag.is_some();
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
return self.text_chunks.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#[cfg(feature = "id3v2")]
|
||||
TagType::ID3v2 => self.id3v2_tag.is_some(),
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
TagType::AIFFText => self.text_chunks.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AiffFile {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag, ID3v2Tag;
|
||||
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
text_chunks, AIFFTextChunks
|
||||
}
|
||||
}
|
||||
|
|
|
@ -167,7 +167,7 @@ where
|
|||
Ok(AiffFile {
|
||||
properties,
|
||||
#[cfg(feature = "aiff_text_chunks")]
|
||||
text_chunks: match text_chunks {
|
||||
text_chunks_tag: match text_chunks {
|
||||
AIFFTextChunks {
|
||||
name: None,
|
||||
author: None,
|
||||
|
|
|
@ -457,7 +457,7 @@ mod tests {
|
|||
|
||||
let parsed_tag = super::super::read::read_from(&mut Cursor::new(tag), false)
|
||||
.unwrap()
|
||||
.text_chunks
|
||||
.text_chunks_tag
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(expected_tag, parsed_tag);
|
||||
|
@ -468,7 +468,7 @@ mod tests {
|
|||
let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.aiff_text");
|
||||
let parsed_tag = super::super::read::read_from(&mut Cursor::new(tag), false)
|
||||
.unwrap()
|
||||
.text_chunks
|
||||
.text_chunks_tag
|
||||
.unwrap();
|
||||
|
||||
// Create a fake AIFF signature
|
||||
|
@ -479,7 +479,7 @@ mod tests {
|
|||
|
||||
let temp_parsed_tag = super::super::read::read_from(&mut Cursor::new(writer), false)
|
||||
.unwrap()
|
||||
.text_chunks
|
||||
.text_chunks_tag
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(parsed_tag, temp_parsed_tag);
|
||||
|
@ -492,7 +492,7 @@ mod tests {
|
|||
|
||||
let aiff_text = super::super::read::read_from(&mut Cursor::new(tag_bytes), false)
|
||||
.unwrap()
|
||||
.text_chunks
|
||||
.text_chunks_tag
|
||||
.unwrap();
|
||||
|
||||
let tag: Tag = aiff_text.into();
|
||||
|
@ -547,10 +547,9 @@ mod tests {
|
|||
let tag_bytes =
|
||||
crate::tag::utils::test_utils::read_path("tests/tags/assets/zero.aiff_text");
|
||||
|
||||
let aiff_text = super::super::read::read_from(&mut Cursor::new(tag_bytes), false)
|
||||
.unwrap()
|
||||
.text_chunks
|
||||
.unwrap();
|
||||
let aiff_file = super::super::read::read_from(&mut Cursor::new(tag_bytes), false).unwrap();
|
||||
|
||||
let aiff_text = aiff_file.text_chunks().unwrap();
|
||||
|
||||
assert_eq!(aiff_text.name, Some(String::new()));
|
||||
assert_eq!(aiff_text.author, Some(String::new()));
|
||||
|
|
|
@ -2,14 +2,11 @@ mod properties;
|
|||
mod read;
|
||||
pub(crate) mod write;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::tag::ID3v2Tag;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
cfg_if::cfg_if! {
|
||||
if #[cfg(feature = "riff_info_list")] {
|
||||
|
@ -22,78 +19,17 @@ cfg_if::cfg_if! {
|
|||
pub use crate::iff::wav::properties::{WavFormat, WavProperties};
|
||||
|
||||
/// A WAV file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct WavFile {
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
/// A RIFF INFO LIST
|
||||
pub(crate) riff_info: Option<RIFFInfoList>,
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
#[lofty(tag_type = "RIFFInfo")]
|
||||
pub(crate) riff_info_tag: Option<RIFFInfoList>,
|
||||
/// An ID3v2 tag
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
pub(crate) id3v2_tag: Option<ID3v2Tag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: WavProperties,
|
||||
}
|
||||
|
||||
impl From<WavFile> for TaggedFile {
|
||||
#[allow(unused_mut)]
|
||||
fn from(input: WavFile) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
tags.push(input.riff_info.map(Into::into));
|
||||
#[cfg(feature = "id3v2")]
|
||||
tags.push(input.id3v2_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::WAV,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for WavFile {
|
||||
type Properties = WavProperties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
Self: Sized,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "id3v2")]
|
||||
return self.id3v2_tag.is_some();
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
return self.riff_info.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#[cfg(feature = "id3v2")]
|
||||
TagType::ID3v2 => self.id3v2_tag.is_some(),
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
TagType::RIFFInfo => self.riff_info.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WavFile {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag, ID3v2Tag;
|
||||
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
riff_info, RIFFInfoList
|
||||
}
|
||||
}
|
||||
|
|
|
@ -128,7 +128,7 @@ where
|
|||
Ok(WavFile {
|
||||
properties,
|
||||
#[cfg(feature = "riff_info_list")]
|
||||
riff_info: (!riff_info.items.is_empty()).then(|| riff_info),
|
||||
riff_info_tag: (!riff_info.items.is_empty()).then(|| riff_info),
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag,
|
||||
})
|
||||
|
|
|
@ -174,6 +174,9 @@
|
|||
|
||||
// TODO: Give 1.62.0 some time, and start using #[default] on enums
|
||||
|
||||
// proc macro hacks
|
||||
extern crate self as lofty;
|
||||
|
||||
pub mod ape;
|
||||
pub mod error;
|
||||
pub(crate) mod file;
|
||||
|
@ -187,6 +190,7 @@ pub mod ogg;
|
|||
pub(crate) mod picture;
|
||||
mod probe;
|
||||
pub(crate) mod properties;
|
||||
pub mod resolve;
|
||||
pub(crate) mod tag;
|
||||
mod traits;
|
||||
pub mod wavpack;
|
||||
|
|
|
@ -1,35 +1,3 @@
|
|||
macro_rules! tag_methods {
|
||||
(
|
||||
$(
|
||||
$(#[cfg($meta:meta)])?
|
||||
$name:ident,
|
||||
$ty:ty
|
||||
);*
|
||||
) => {
|
||||
paste::paste! {
|
||||
$(
|
||||
$(#[cfg($meta)])?
|
||||
#[doc = "Gets the [`" $ty "`] if it exists"]
|
||||
pub fn $name(&self) -> Option<&$ty> {
|
||||
self.$name.as_ref()
|
||||
}
|
||||
|
||||
$(#[cfg($meta)])?
|
||||
#[doc = "Gets a mutable reference to the [`" $ty "`] if it exists"]
|
||||
pub fn [<$name _mut>](&mut self) -> Option<&mut $ty> {
|
||||
self.$name.as_mut()
|
||||
}
|
||||
|
||||
$(#[cfg($meta)])?
|
||||
#[doc = "Removes the [`" $ty "`]"]
|
||||
pub fn [<remove_ $name>](&mut self) {
|
||||
self.$name = None
|
||||
}
|
||||
)*
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// See cfg-if comment in `Cargo.toml`
|
||||
//
|
||||
// macro_rules! feature_locked {
|
||||
|
@ -72,4 +40,4 @@ macro_rules! err {
|
|||
};
|
||||
}
|
||||
|
||||
pub(crate) use {err, tag_methods, try_vec};
|
||||
pub(crate) use {err, try_vec};
|
||||
|
|
|
@ -10,101 +10,30 @@ pub use properties::Mp3Properties;
|
|||
|
||||
#[cfg(feature = "ape")]
|
||||
use crate::ape::tag::ApeTag;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v1")]
|
||||
use crate::id3::v1::tag::ID3v1Tag;
|
||||
#[cfg(feature = "id3v2")]
|
||||
use crate::id3::v2::tag::ID3v2Tag;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
/// An MP3 file
|
||||
#[derive(Default)]
|
||||
#[derive(Default, LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct Mp3File {
|
||||
#[cfg(feature = "id3v2")]
|
||||
/// An ID3v2 tag
|
||||
#[cfg(feature = "id3v2")]
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
pub(crate) id3v2_tag: Option<ID3v2Tag>,
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// An ID3v1 tag
|
||||
#[cfg(feature = "id3v1")]
|
||||
#[lofty(tag_type = "ID3v1")]
|
||||
pub(crate) id3v1_tag: Option<ID3v1Tag>,
|
||||
#[cfg(feature = "ape")]
|
||||
/// An APEv1/v2 tag
|
||||
#[cfg(feature = "ape")]
|
||||
#[lofty(tag_type = "APE")]
|
||||
pub(crate) ape_tag: Option<ApeTag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: Mp3Properties,
|
||||
}
|
||||
|
||||
impl From<Mp3File> for TaggedFile {
|
||||
#[allow(clippy::vec_init_then_push, unused_mut)]
|
||||
fn from(input: Mp3File) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(3);
|
||||
|
||||
#[cfg(feature = "id3v2")]
|
||||
tags.push(input.id3v2_tag.map(Into::into));
|
||||
#[cfg(feature = "id3v1")]
|
||||
tags.push(input.id3v1_tag.map(Into::into));
|
||||
#[cfg(feature = "ape")]
|
||||
tags.push(input.ape_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::MP3,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for Mp3File {
|
||||
type Properties = Mp3Properties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "id3v2")]
|
||||
return self.id3v2_tag.is_some();
|
||||
#[cfg(feature = "id3v1")]
|
||||
return self.id3v1_tag.is_some();
|
||||
#[cfg(feature = "ape")]
|
||||
return self.ape_tag.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::APE => self.ape_tag.is_some(),
|
||||
#[cfg(feature = "id3v2")]
|
||||
TagType::ID3v2 => self.id3v2_tag.is_some(),
|
||||
#[cfg(feature = "id3v1")]
|
||||
TagType::ID3v1 => self.id3v1_tag.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Mp3File {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "id3v2")]
|
||||
id3v2_tag, ID3v2Tag;
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
id3v1_tag, ID3v1Tag;
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag, ApeTag
|
||||
}
|
||||
}
|
||||
|
|
|
@ -786,7 +786,7 @@ mod tests {
|
|||
let file_bytes = read_path("tests/files/assets/non_full_meta_atom.m4a");
|
||||
let file = Mp4File::read_from(&mut Cursor::new(file_bytes), false).unwrap();
|
||||
|
||||
assert!(file.ilst.is_some());
|
||||
assert!(file.ilst_tag.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -809,10 +809,10 @@ mod tests {
|
|||
file.rewind().unwrap();
|
||||
|
||||
let mp4_file = Mp4File::read_from(&mut file, true).unwrap();
|
||||
assert!(mp4_file.ilst.is_some());
|
||||
assert!(mp4_file.ilst_tag.is_some());
|
||||
|
||||
verify_atom(
|
||||
&mp4_file.ilst.unwrap(),
|
||||
&mp4_file.ilst_tag.unwrap(),
|
||||
*b"\xa9ART",
|
||||
&AtomData::UTF8(String::from("Foo artist")),
|
||||
);
|
||||
|
@ -847,6 +847,6 @@ mod tests {
|
|||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(file.ilst, Some(Ilst::default()));
|
||||
assert_eq!(file.ilst(), Some(&Ilst::default()));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,12 +9,9 @@ mod properties;
|
|||
mod read;
|
||||
mod trak;
|
||||
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
// Exports
|
||||
|
||||
|
@ -38,67 +35,19 @@ cfg_if::cfg_if! {
|
|||
pub use crate::mp4::properties::{AudioObjectType, Mp4Codec, Mp4Properties};
|
||||
|
||||
/// An MP4 file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct Mp4File {
|
||||
/// The file format from ftyp's "major brand" (Ex. "M4A ")
|
||||
pub(crate) ftyp: String,
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
#[lofty(tag_type = "MP4ilst")]
|
||||
/// The parsed `ilst` (metadata) atom, if it exists
|
||||
pub(crate) ilst: Option<Ilst>,
|
||||
pub(crate) ilst_tag: Option<Ilst>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: Mp4Properties,
|
||||
}
|
||||
|
||||
impl From<Mp4File> for TaggedFile {
|
||||
fn from(input: Mp4File) -> Self {
|
||||
Self {
|
||||
ty: FileType::MP4,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: {
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
if let Some(ilst) = input.ilst {
|
||||
vec![ilst.into()]
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "mp4_ilst"))]
|
||||
Vec::new()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for Mp4File {
|
||||
type Properties = Mp4Properties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
return self.ilst.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
#[allow(unreachable_code, unused_variables)]
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
return tag_type == TagType::MP4ilst && self.ilst.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Mp4File {
|
||||
/// Returns the file format from ftyp's "major brand" (Ex. "M4A ")
|
||||
///
|
||||
|
@ -119,10 +68,3 @@ impl Mp4File {
|
|||
self.ftyp.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl Mp4File {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
ilst, Ilst
|
||||
}
|
||||
}
|
||||
|
|
|
@ -164,7 +164,7 @@ where
|
|||
Ok(Mp4File {
|
||||
ftyp,
|
||||
#[cfg(feature = "mp4_ilst")]
|
||||
ilst: moov.meta,
|
||||
ilst_tag: moov.meta,
|
||||
properties: if read_properties {
|
||||
super::properties::read_properties(&mut reader, &moov.traks, file_length)?
|
||||
} else {
|
||||
|
|
|
@ -4,38 +4,30 @@ use super::find_last_page;
|
|||
#[cfg(feature = "vorbis_comments")]
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::file::AudioFile;
|
||||
use crate::ogg::constants::{OPUSHEAD, OPUSTAGS};
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::TagType;
|
||||
use properties::OpusProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
/// An OGG Opus file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(no_audiofile_impl)]
|
||||
pub struct OpusFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
|
||||
pub(crate) vorbis_comments: VorbisComments,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
#[lofty(tag_type = "VorbisComments")]
|
||||
#[lofty(always_present)]
|
||||
pub(crate) vorbis_comments_tag: VorbisComments,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: OpusProperties,
|
||||
}
|
||||
|
||||
impl From<OpusFile> for TaggedFile {
|
||||
fn from(input: OpusFile) -> Self {
|
||||
Self {
|
||||
ty: FileType::Opus,
|
||||
properties: FileProperties::from(input.properties),
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
tags: vec![input.vorbis_comments.into()],
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for OpusFile {
|
||||
type Properties = OpusProperties;
|
||||
|
||||
|
@ -49,7 +41,7 @@ impl AudioFile for OpusFile {
|
|||
properties: if read_properties {properties::read_properties(reader, &file_information.1)? } else { OpusProperties::default() },
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
// Safe to unwrap, a metadata packet is mandatory in Opus
|
||||
vorbis_comments: file_information.0.unwrap(),
|
||||
vorbis_comments_tag: file_information.0.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -65,17 +57,3 @@ impl AudioFile for OpusFile {
|
|||
tag_type == TagType::VorbisComments
|
||||
}
|
||||
}
|
||||
|
||||
impl OpusFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments(&self) -> &VorbisComments {
|
||||
&self.vorbis_comments
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
|
||||
&mut self.vorbis_comments
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,38 +3,30 @@ pub(super) mod properties;
|
|||
#[cfg(feature = "vorbis_comments")]
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::file::AudioFile;
|
||||
use crate::ogg::constants::SPEEXHEADER;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::TagType;
|
||||
use properties::SpeexProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
/// An OGG Speex file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(no_audiofile_impl)]
|
||||
pub struct SpeexFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
|
||||
pub(crate) vorbis_comments: VorbisComments,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
#[lofty(tag_type = "VorbisComments")]
|
||||
#[lofty(always_present)]
|
||||
pub(crate) vorbis_comments_tag: VorbisComments,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: SpeexProperties,
|
||||
}
|
||||
|
||||
impl From<SpeexFile> for TaggedFile {
|
||||
fn from(input: SpeexFile) -> Self {
|
||||
Self {
|
||||
ty: FileType::Speex,
|
||||
properties: FileProperties::from(input.properties),
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
tags: vec![input.vorbis_comments.into()],
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for SpeexFile {
|
||||
type Properties = SpeexProperties;
|
||||
|
||||
|
@ -48,7 +40,7 @@ impl AudioFile for SpeexFile {
|
|||
properties: if read_properties { properties::read_properties(reader, &file_information.1)? } else { SpeexProperties::default() },
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
// Safe to unwrap, a metadata packet is mandatory in Speex
|
||||
vorbis_comments: file_information.0.unwrap(),
|
||||
vorbis_comments_tag: file_information.0.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -64,17 +56,3 @@ impl AudioFile for SpeexFile {
|
|||
tag_type == TagType::VorbisComments
|
||||
}
|
||||
}
|
||||
|
||||
impl SpeexFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments(&self) -> &VorbisComments {
|
||||
&self.vorbis_comments
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
|
||||
&mut self.vorbis_comments
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,38 +6,30 @@ use super::find_last_page;
|
|||
#[cfg(feature = "vorbis_comments")]
|
||||
use super::tag::VorbisComments;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::file::AudioFile;
|
||||
use crate::ogg::constants::{VORBIS_COMMENT_HEAD, VORBIS_IDENT_HEAD};
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::TagType;
|
||||
use properties::VorbisProperties;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
/// An OGG Vorbis file
|
||||
#[derive(LoftyFile)]
|
||||
#[lofty(no_audiofile_impl)]
|
||||
pub struct VorbisFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// The vorbis comments contained in the file
|
||||
///
|
||||
/// NOTE: While a metadata packet is required, it isn't required to actually have any data.
|
||||
pub(crate) vorbis_comments: VorbisComments,
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
#[lofty(tag_type = "VorbisComments")]
|
||||
#[lofty(always_present)]
|
||||
pub(crate) vorbis_comments_tag: VorbisComments,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: VorbisProperties,
|
||||
}
|
||||
|
||||
impl From<VorbisFile> for TaggedFile {
|
||||
fn from(input: VorbisFile) -> Self {
|
||||
Self {
|
||||
ty: FileType::Vorbis,
|
||||
properties: FileProperties::from(input.properties),
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
tags: vec![input.vorbis_comments.into()],
|
||||
#[cfg(not(feature = "vorbis_comments"))]
|
||||
tags: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for VorbisFile {
|
||||
type Properties = VorbisProperties;
|
||||
|
||||
|
@ -52,7 +44,7 @@ impl AudioFile for VorbisFile {
|
|||
properties: if read_properties { properties::read_properties(reader, &file_information.1)? } else { VorbisProperties::default() },
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
// Safe to unwrap, a metadata packet is mandatory in OGG Vorbis
|
||||
vorbis_comments: file_information.0.unwrap(),
|
||||
vorbis_comments_tag: file_information.0.unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -68,17 +60,3 @@ impl AudioFile for VorbisFile {
|
|||
tag_type == TagType::VorbisComments
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisFile {
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments(&self) -> &VorbisComments {
|
||||
&self.vorbis_comments
|
||||
}
|
||||
|
||||
#[cfg(feature = "vorbis_comments")]
|
||||
/// Returns a mutable reference to the Vorbis comments tag
|
||||
pub fn vorbis_comments_mut(&mut self) -> &mut VorbisComments {
|
||||
&mut self.vorbis_comments
|
||||
}
|
||||
}
|
||||
|
|
28
src/probe.rs
28
src/probe.rs
|
@ -13,6 +13,7 @@ use crate::ogg::speex::SpeexFile;
|
|||
use crate::ogg::vorbis::VorbisFile;
|
||||
use crate::wavpack::WavPackFile;
|
||||
|
||||
use crate::resolve::CUSTOM_RESOLVERS;
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom};
|
||||
use std::path::Path;
|
||||
|
@ -311,7 +312,18 @@ impl<R: Read + Seek> Probe<R> {
|
|||
|
||||
file_type_after_id3_block
|
||||
},
|
||||
_ => Ok(None),
|
||||
_ => {
|
||||
if let Ok(lock) = CUSTOM_RESOLVERS.lock() {
|
||||
#[allow(clippy::significant_drop_in_scrutinee)]
|
||||
for (_, resolve) in lock.iter() {
|
||||
if let ret @ Some(_) = resolve.guess(&buf[..buf_len]) {
|
||||
return Ok(ret);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,6 +339,10 @@ impl<R: Read + Seek> Probe<R> {
|
|||
/// paths, this is not necessary.
|
||||
/// * The reader contains invalid data
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// If an unregistered `FileType` ([`FileType::Custom`]) is encountered. See [`crate::resolve::register_custom_resolver`].
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
|
@ -356,6 +372,16 @@ impl<R: Read + Seek> Probe<R> {
|
|||
FileType::MP4 => Mp4File::read_from(reader, read_properties)?.into(),
|
||||
FileType::Speex => SpeexFile::read_from(reader, read_properties)?.into(),
|
||||
FileType::WavPack => WavPackFile::read_from(reader, read_properties)?.into(),
|
||||
FileType::Custom(c) => {
|
||||
if let Some(r) = crate::resolve::lookup_resolver(c) {
|
||||
r.read_from(reader, read_properties)?
|
||||
} else {
|
||||
panic!(
|
||||
"Encountered an unregistered custom `FileType` named `{}`",
|
||||
c
|
||||
);
|
||||
}
|
||||
},
|
||||
}),
|
||||
None => err!(UnknownFormat),
|
||||
}
|
||||
|
|
206
src/resolve.rs
Normal file
206
src/resolve.rs
Normal file
|
@ -0,0 +1,206 @@
|
|||
//! Tools to create custom file resolvers
|
||||
//!
|
||||
//! For a full example of a custom resolver, see [this](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver).
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::io::{Read, Seek};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
/// A custom file resolver
|
||||
///
|
||||
/// This trait allows for the creation of custom [`FileType`]s, that can make use of
|
||||
/// lofty's API. Registering a `FileResolver` ([`register_custom_resolver`]) makes it possible
|
||||
/// to detect and read files using [`crate::probe::Probe`].
|
||||
pub trait FileResolver: Send + Sync + AudioFile {
|
||||
/// The extension associated with the [`FileType`] without the '.'
|
||||
fn extension() -> Option<&'static str>;
|
||||
/// The primary [`TagType`] for the [`FileType`]
|
||||
fn primary_tag_type() -> TagType;
|
||||
/// The [`FileType`]'s supported [`TagType`]s
|
||||
fn supported_tag_types() -> &'static [TagType];
|
||||
|
||||
/// Attempts to guess the [`FileType`] from a portion of the file content
|
||||
///
|
||||
/// NOTE: This will only provide (up to) the first 50 bytes of the file
|
||||
fn guess(buf: &[u8]) -> Option<FileType>;
|
||||
}
|
||||
|
||||
// Just broken out to its own type to make `CUSTOM_RESOLVER`'s type shorter :)
|
||||
type ResolverMap = HashMap<&'static str, &'static dyn ObjectSafeFileResolver>;
|
||||
|
||||
pub(crate) static CUSTOM_RESOLVERS: Lazy<Arc<Mutex<ResolverMap>>> =
|
||||
Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
|
||||
|
||||
pub(crate) fn lookup_resolver(name: &'static str) -> Option<&'static dyn ObjectSafeFileResolver> {
|
||||
let res = CUSTOM_RESOLVERS.lock().ok()?;
|
||||
|
||||
res.get(name).copied()
|
||||
}
|
||||
|
||||
// A `Read + Seek` supertrait for use in [`ObjectSafeFileResolver::read_from`]
|
||||
pub(crate) trait SeekRead: Read + Seek {}
|
||||
impl<T: Seek + Read> SeekRead for T {}
|
||||
|
||||
// `FileResolver` isn't object safe itself, so we need this wrapper trait
|
||||
pub(crate) trait ObjectSafeFileResolver: Send + Sync {
|
||||
fn extension(&self) -> Option<&'static str>;
|
||||
fn primary_tag_type(&self) -> TagType;
|
||||
fn supported_tag_types(&self) -> &'static [TagType];
|
||||
fn guess(&self, buf: &[u8]) -> Option<FileType>;
|
||||
|
||||
// A mask for the `AudioFile::read_from` impl
|
||||
fn read_from(&self, reader: &mut dyn SeekRead, read_properties: bool) -> Result<TaggedFile>;
|
||||
}
|
||||
|
||||
// A fake `FileResolver` implementer, so we don't need to construct the type in `register_custom_resolver`
|
||||
pub(crate) struct GhostlyResolver<T: 'static>(PhantomData<T>);
|
||||
impl<T: FileResolver> ObjectSafeFileResolver for GhostlyResolver<T> {
|
||||
fn extension(&self) -> Option<&'static str> {
|
||||
T::extension()
|
||||
}
|
||||
|
||||
fn primary_tag_type(&self) -> TagType {
|
||||
T::primary_tag_type()
|
||||
}
|
||||
|
||||
fn supported_tag_types(&self) -> &'static [TagType] {
|
||||
T::supported_tag_types()
|
||||
}
|
||||
|
||||
fn guess(&self, buf: &[u8]) -> Option<FileType> {
|
||||
T::guess(buf)
|
||||
}
|
||||
|
||||
fn read_from(&self, reader: &mut dyn SeekRead, read_properties: bool) -> Result<TaggedFile> {
|
||||
Ok(<T as AudioFile>::read_from(&mut Box::new(reader), read_properties)?.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a custom file resolver
|
||||
///
|
||||
/// Provided a type and a name to associate it with, this will attempt
|
||||
/// to load them into the resolver collection.
|
||||
///
|
||||
/// Conditions:
|
||||
/// * Both the resolver and name *must* be static.
|
||||
/// * `name` **must** match the name of your custom [`FileType`] variant (case sensitive!)
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// * Attempting to register an existing name or type (See [`remove_custom_resolver`])
|
||||
/// * See [`Mutex::lock`]
|
||||
pub fn register_custom_resolver<T: FileResolver + 'static>(name: &'static str) {
|
||||
let mut res = CUSTOM_RESOLVERS.lock().unwrap();
|
||||
assert!(
|
||||
res.iter().all(|(n, _)| *n != name),
|
||||
"Resolver `{}` already exists!",
|
||||
name
|
||||
);
|
||||
|
||||
let ghost = GhostlyResolver::<T>(PhantomData::default());
|
||||
let b: Box<dyn ObjectSafeFileResolver> = Box::new(ghost);
|
||||
|
||||
res.insert(name, Box::leak::<'static>(b));
|
||||
}
|
||||
|
||||
/// Remove a registered file resolver
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// See [`Mutex::lock`]
|
||||
pub fn remove_custom_resolver(name: &'static str) {
|
||||
let mut resolvers = CUSTOM_RESOLVERS.lock().unwrap();
|
||||
|
||||
if let Some(res) = resolvers.remove(name) {
|
||||
unsafe {
|
||||
#[allow(trivial_casts)]
|
||||
let b = Box::from_raw(res as *const _ as *mut dyn ObjectSafeFileResolver);
|
||||
drop(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::id3::v2::ID3v2Tag;
|
||||
use crate::resolve::{register_custom_resolver, FileResolver};
|
||||
use crate::{Accessor, FileProperties, FileType, TagType};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::{Read, Seek};
|
||||
use std::panic;
|
||||
|
||||
#[derive(LoftyFile, Default)]
|
||||
#[lofty(read_fn = "Self::read")]
|
||||
#[lofty(file_type = "Custom(\"MyFile\")")]
|
||||
struct MyFile {
|
||||
#[lofty(tag_type = "ID3v2")]
|
||||
id3v2_tag: Option<ID3v2Tag>,
|
||||
properties: FileProperties,
|
||||
}
|
||||
|
||||
impl FileResolver for MyFile {
|
||||
fn extension() -> Option<&'static str> {
|
||||
Some("myfile")
|
||||
}
|
||||
|
||||
fn primary_tag_type() -> TagType {
|
||||
TagType::ID3v2
|
||||
}
|
||||
|
||||
fn supported_tag_types() -> &'static [TagType] {
|
||||
&[TagType::ID3v2]
|
||||
}
|
||||
|
||||
fn guess(buf: &[u8]) -> Option<FileType> {
|
||||
if buf.starts_with(b"myfile") {
|
||||
return Some(FileType::Custom("MyFile"));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MyFile {
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
fn read<R: Read + Seek + ?Sized>(
|
||||
_reader: &mut R,
|
||||
_read_properties: bool,
|
||||
) -> crate::error::Result<Self> {
|
||||
let mut tag = ID3v2Tag::default();
|
||||
tag.set_artist(String::from("All is well!"));
|
||||
|
||||
Ok(Self {
|
||||
id3v2_tag: Some(tag),
|
||||
properties: FileProperties::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_resolver() {
|
||||
register_custom_resolver::<MyFile>("MyFile");
|
||||
|
||||
let path = "examples/custom_resolver/test_asset.myfile";
|
||||
let read = crate::read_from_path(path, false).unwrap();
|
||||
assert_eq!(read.file_type(), FileType::Custom("MyFile"));
|
||||
|
||||
let read_content = crate::read_from(&mut File::open(path).unwrap(), false).unwrap();
|
||||
assert_eq!(read_content.file_type(), FileType::Custom("MyFile"));
|
||||
|
||||
assert!(
|
||||
panic::catch_unwind(|| {
|
||||
register_custom_resolver::<MyFile>("MyFile");
|
||||
})
|
||||
.is_err(),
|
||||
"We didn't panic on double register!"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -5,90 +5,27 @@ pub(crate) mod write;
|
|||
|
||||
#[cfg(feature = "ape")]
|
||||
use crate::ape::tag::ApeTag;
|
||||
use crate::error::Result;
|
||||
use crate::file::{AudioFile, FileType, TaggedFile};
|
||||
#[cfg(feature = "id3v1")]
|
||||
use crate::id3::v1::tag::ID3v1Tag;
|
||||
use crate::properties::FileProperties;
|
||||
use crate::tag::{Tag, TagType};
|
||||
use crate::tag::TagType;
|
||||
|
||||
use std::io::{Read, Seek};
|
||||
use lofty_attr::LoftyFile;
|
||||
|
||||
// Exports
|
||||
pub use properties::WavPackProperties;
|
||||
|
||||
/// A WavPack file
|
||||
#[derive(Default)]
|
||||
#[derive(Default, LoftyFile)]
|
||||
#[lofty(read_fn = "read::read_from")]
|
||||
pub struct WavPackFile {
|
||||
#[cfg(feature = "id3v1")]
|
||||
/// An ID3v1 tag
|
||||
#[cfg(feature = "id3v1")]
|
||||
#[lofty(tag_type = "ID3v1")]
|
||||
pub(crate) id3v1_tag: Option<ID3v1Tag>,
|
||||
#[cfg(feature = "ape")]
|
||||
/// An APEv1/v2 tag
|
||||
#[cfg(feature = "ape")]
|
||||
#[lofty(tag_type = "APE")]
|
||||
pub(crate) ape_tag: Option<ApeTag>,
|
||||
/// The file's audio properties
|
||||
pub(crate) properties: WavPackProperties,
|
||||
}
|
||||
|
||||
impl From<WavPackFile> for TaggedFile {
|
||||
#[allow(clippy::vec_init_then_push, unused_mut)]
|
||||
fn from(input: WavPackFile) -> Self {
|
||||
let mut tags = Vec::<Option<Tag>>::with_capacity(2);
|
||||
|
||||
#[cfg(feature = "id3v1")]
|
||||
tags.push(input.id3v1_tag.map(Into::into));
|
||||
#[cfg(feature = "ape")]
|
||||
tags.push(input.ape_tag.map(Into::into));
|
||||
|
||||
Self {
|
||||
ty: FileType::WavPack,
|
||||
properties: FileProperties::from(input.properties),
|
||||
tags: tags.into_iter().flatten().collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AudioFile for WavPackFile {
|
||||
type Properties = WavPackProperties;
|
||||
|
||||
fn read_from<R>(reader: &mut R, read_properties: bool) -> Result<Self>
|
||||
where
|
||||
R: Read + Seek,
|
||||
{
|
||||
read::read_from(reader, read_properties)
|
||||
}
|
||||
|
||||
fn properties(&self) -> &Self::Properties {
|
||||
&self.properties
|
||||
}
|
||||
|
||||
#[allow(unreachable_code)]
|
||||
fn contains_tag(&self) -> bool {
|
||||
#[cfg(feature = "id3v1")]
|
||||
return self.id3v1_tag.is_some();
|
||||
#[cfg(feature = "ape")]
|
||||
return self.ape_tag.is_some();
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn contains_tag_type(&self, tag_type: TagType) -> bool {
|
||||
match tag_type {
|
||||
#[cfg(feature = "ape")]
|
||||
TagType::APE => self.ape_tag.is_some(),
|
||||
#[cfg(feature = "id3v1")]
|
||||
TagType::ID3v1 => self.id3v1_tag.is_some(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WavPackFile {
|
||||
crate::macros::tag_methods! {
|
||||
#[cfg(feature = "id3v1")]
|
||||
id3v1_tag, ID3v1Tag;
|
||||
|
||||
#[cfg(feature = "ape")]
|
||||
ape_tag, ApeTag
|
||||
}
|
||||
}
|
||||
|
|
|
@ -130,8 +130,8 @@ fn flac_with_id3v2() {
|
|||
let file = std::fs::read("tests/files/assets/flac_with_id3v2.flac").unwrap();
|
||||
let flac_file = FlacFile::read_from(&mut std::io::Cursor::new(file), true).unwrap();
|
||||
|
||||
assert!(flac_file.id3v2_tag().is_some());
|
||||
assert_eq!(flac_file.id3v2_tag().unwrap().artist(), Some("Foo artist"));
|
||||
assert!(flac_file.id3v2().is_some());
|
||||
assert_eq!(flac_file.id3v2().unwrap().artist(), Some("Foo artist"));
|
||||
|
||||
assert!(flac_file.vorbis_comments().is_some());
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue