Implement custom file resolvers (#40)

This commit is contained in:
Alex 2022-07-24 16:08:46 -04:00 committed by GitHub
parent 85ad435898
commit f48014fda8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 995 additions and 663 deletions

View file

@ -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

View file

@ -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"]

View file

@ -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

View 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" }

View 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();
}

View file

@ -0,0 +1 @@
myfiledata

12
lofty_attr/Cargo.toml Normal file
View 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
View 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)
}

View file

@ -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
}
}

View file

@ -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
}
},
}
}

View file

@ -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
}
}

View file

@ -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);
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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()));

View file

@ -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
}
}

View file

@ -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,
})

View file

@ -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;

View file

@ -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};

View file

@ -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
}
}

View file

@ -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()));
}
}

View file

@ -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
}
}

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
View 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!"
);
}
}

View file

@ -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
}
}

View file

@ -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());
}