lofty_attr: Add supported_formats attribute for tags

This commit is contained in:
Serial 2022-10-04 00:34:20 -04:00
parent 0b6ad1599b
commit 3a4b4f243e
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
15 changed files with 466 additions and 370 deletions

View file

@ -1,6 +1,6 @@
// Items that only pertain to internal usage of lofty_attr // Items that only pertain to internal usage of lofty_attr
use crate::FieldContents; use crate::lofty_file::FieldContents;
use std::collections::HashMap; use std::collections::HashMap;

View file

@ -1,22 +1,52 @@
mod internal; mod internal;
mod lofty_file;
mod lofty_tag;
mod util; mod util;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::Span; use quote::quote;
use quote::{format_ident, quote, quote_spanned, ToTokens}; use syn::{parse_macro_input, Data, DataStruct, DeriveInput, Fields};
use syn::spanned::Spanned;
use syn::{parse_macro_input, Attribute, Data, DataStruct, DeriveInput, Fields, Ident, Type};
/// Creates a file usable by Lofty /// Creates a file usable by Lofty
/// ///
/// See [here](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver) for an example of how to use it. /// See [here](https://github.com/Serial-ATA/lofty-rs/tree/main/examples/custom_resolver) for an example of how to use it.
// TODO: #[internal]
#[proc_macro_derive(LoftyFile, attributes(lofty))] #[proc_macro_derive(LoftyFile, attributes(lofty))]
pub fn lofty_file(input: TokenStream) -> TokenStream { pub fn lofty_file(input: TokenStream) -> TokenStream {
act(input, lofty_file::parse)
}
#[proc_macro_derive(LoftyTag, attributes(lofty))]
#[doc(hidden)]
pub fn lofty_tag(input: TokenStream) -> TokenStream {
act(input, lofty_tag::parse)
}
fn act(
input: TokenStream,
func: impl Fn(&DeriveInput, &DataStruct, &mut Vec<syn::Error>) -> proc_macro2::TokenStream,
) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput); let input = parse_macro_input!(input as DeriveInput);
let data_struct = match input.data {
Data::Struct(
ref data_struct @ DataStruct {
fields: Fields::Named(_),
..
},
) => data_struct,
_ => {
return TokenStream::from(
util::err(
input.ident.span(),
"This macro can only be used on structs with named fields",
)
.to_compile_error(),
);
},
};
let mut errors = Vec::new(); let mut errors = Vec::new();
let ret = parse(input, &mut errors); let ret = func(&input, data_struct, &mut errors);
let compile_errors = errors.iter().map(syn::Error::to_compile_error); let compile_errors = errors.iter().map(syn::Error::to_compile_error);
@ -25,353 +55,3 @@ pub fn lofty_file(input: TokenStream) -> TokenStream {
#ret #ret
}) })
} }
fn parse(input: DeriveInput, errors: &mut Vec<syn::Error>) -> proc_macro2::TokenStream {
macro_rules! bail {
($errors:ident, $span:expr, $msg:literal) => {
$errors.push(util::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 util::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();
// TODO: This is not readable in the slightest
let opt_file_type = internal::opt_internal_file_type(struct_name.to_string());
let has_internal_file_type = opt_file_type.is_some();
let is_internal = input
.attrs
.iter()
.any(|attr| util::has_path_attr(attr, "internal_write_module_do_not_use_anywhere_else"));
if opt_file_type.is_none() && is_internal {
// TODO: This is the best check we can do for now I think?
// Definitely needs some work when a better solution comes out.
bail!(
errors,
input.ident.span(),
"Attempted to use an internal attribute externally"
);
}
let mut id3v2_strippable = false;
let file_type = match opt_file_type {
Some((ft, id3v2_strip)) => {
id3v2_strippable = id3v2_strip;
ft
},
_ => match util::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(util::err(input.ident.span(), "Struct has no tag fields"));
}
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 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 audiofile_impl = if impl_audiofile {
quote! {
impl lofty::AudioFile for #struct_name {
type Properties = #properties_field_ty;
fn read_from<R>(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result<Self>
where
R: std::io::Read + std::io::Seek,
{
#read_fn(reader, parse_options)
}
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: lofty::TagType) -> bool {
match tag_type {
#( lofty::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);
let file_type_variant = if has_internal_file_type {
quote! { lofty::FileType::#file_type }
} else {
let file_ty_str = file_type.to_string();
quote! { lofty::FileType::Custom(#file_ty_str) }
};
let mut ret = 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(
#file_type_variant,
lofty::FileProperties::from(input.properties),
{
let mut tags: Vec<lofty::Tag> = Vec::new();
#( #conditions )*
tags
}
)
}
}
#( #getters )*
};
// Create `write` module if internal
if is_internal {
let lookup = internal::init_write_lookup(id3v2_strippable);
let write_mod = internal::write_module(&tag_fields, lookup);
ret = quote! {
#ret
#write_mod
}
}
ret
}
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 util::get_attr("tag_type", &field.attrs) {
Some(tt) => tt,
_ => {
errors.push(util::err(field.span(), "Field has no `tag_type` attribute"));
return None;
},
};
let cfg = field
.attrs
.iter()
.cloned()
.filter_map(|a| util::get_attr_list("cfg", &a).map(|_| a))
.collect::<Vec<_>>();
let option_unwrapped = util::extract_type_from_option(&field.ty);
// `option_unwrapped` will be `Some` if the type was wrapped in an `Option`
let needs_option = option_unwrapped.is_some();
let contents = FieldContents {
name,
getter_name: util::get_attr("getter", &field.attrs),
ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()),
tag_type,
needs_option,
cfg_features: cfg,
};
tag_fields.push(contents);
continue;
}
if name == "properties" {
properties_field = Some(field);
}
}
Some((tag_fields, properties_field))
}
fn should_impl_audiofile(attrs: &[Attribute]) -> bool {
for attr in attrs {
if util::has_path_attr(attr, "no_audiofile_impl") {
return false;
}
}
true
}
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 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 {
let assert_field_ty_default = quote_spanned! {f.name.span()=>
struct _AssertDefault where #field_ty: core::default::Default;
};
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
}
}
}
})
}

View file

@ -0,0 +1,338 @@
use crate::internal;
use crate::util::{self, bail};
use proc_macro2::{Ident, Span};
use quote::{format_ident, quote, quote_spanned, ToTokens};
use syn::spanned::Spanned;
use syn::{Attribute, DataStruct, DeriveInput, Type};
pub(crate) fn parse(
input: &DeriveInput,
data_struct: &DataStruct,
errors: &mut Vec<syn::Error>,
) -> proc_macro2::TokenStream {
let impl_audiofile = should_impl_audiofile(&input.attrs);
let read_fn = match util::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();
// TODO: This is not readable in the slightest
let opt_file_type = internal::opt_internal_file_type(struct_name.to_string());
let has_internal_file_type = opt_file_type.is_some();
let is_internal = input
.attrs
.iter()
.any(|attr| util::has_path_attr(attr, "internal_write_module_do_not_use_anywhere_else"));
if opt_file_type.is_none() && is_internal {
// TODO: This is the best check we can do for now I think?
// Definitely needs some work when a better solution comes out.
bail!(
errors,
input.ident.span(),
"Attempted to use an internal attribute externally"
);
}
let mut id3v2_strippable = false;
let file_type = match opt_file_type {
Some((ft, id3v2_strip)) => {
id3v2_strippable = id3v2_strip;
ft
},
_ => match util::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_struct) {
Some(fields) => fields,
None => return proc_macro2::TokenStream::new(),
};
if tag_fields.is_empty() {
errors.push(util::err(input.ident.span(), "Struct has no tag fields"));
}
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 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 audiofile_impl = if impl_audiofile {
quote! {
impl lofty::AudioFile for #struct_name {
type Properties = #properties_field_ty;
fn read_from<R>(reader: &mut R, parse_options: lofty::ParseOptions) -> lofty::error::Result<Self>
where
R: std::io::Read + std::io::Seek,
{
#read_fn(reader, parse_options)
}
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: lofty::TagType) -> bool {
match tag_type {
#( lofty::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);
let file_type_variant = if has_internal_file_type {
quote! { lofty::FileType::#file_type }
} else {
let file_ty_str = file_type.to_string();
quote! { lofty::FileType::Custom(#file_ty_str) }
};
let mut ret = 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(
#file_type_variant,
lofty::FileProperties::from(input.properties),
{
let mut tags: Vec<lofty::Tag> = Vec::new();
#( #conditions )*
tags
}
)
}
}
#( #getters )*
};
// Create `write` module if internal
if is_internal {
let lookup = internal::init_write_lookup(id3v2_strippable);
let write_mod = internal::write_module(&tag_fields, lookup);
ret = quote! {
#ret
#write_mod
}
}
ret
}
pub(crate) struct FieldContents {
name: Ident,
pub(crate) cfg_features: Vec<Attribute>,
needs_option: bool,
getter_name: Option<proc_macro2::TokenStream>,
ty: Type,
pub(crate) 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 util::get_attr("tag_type", &field.attrs) {
Some(tt) => tt,
_ => {
errors.push(util::err(field.span(), "Field has no `tag_type` attribute"));
return None;
},
};
let cfg = field
.attrs
.iter()
.cloned()
.filter_map(|a| util::get_attr_list("cfg", &a).map(|_| a))
.collect::<Vec<_>>();
let option_unwrapped = util::extract_type_from_option(&field.ty);
// `option_unwrapped` will be `Some` if the type was wrapped in an `Option`
let needs_option = option_unwrapped.is_some();
let contents = FieldContents {
name,
getter_name: util::get_attr("getter", &field.attrs),
ty: option_unwrapped.unwrap_or_else(|| field.ty.clone()),
tag_type,
needs_option,
cfg_features: cfg,
};
tag_fields.push(contents);
continue;
}
if name == "properties" {
properties_field = Some(field);
}
}
Some((tag_fields, properties_field))
}
fn should_impl_audiofile(attrs: &[Attribute]) -> bool {
for attr in attrs {
if util::has_path_attr(attr, "no_audiofile_impl") {
return false;
}
}
true
}
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 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 {
let assert_field_ty_default = quote_spanned! {f.name.span()=>
struct _AssertDefault where #field_ty: core::default::Default;
};
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
}
}
}
})
}

View file

@ -0,0 +1,47 @@
use crate::util::{self, bail};
use quote::quote;
use syn::{DataStruct, DeriveInput, Meta, NestedMeta};
pub(crate) fn parse(
input: &DeriveInput,
_data_struct: &DataStruct,
errors: &mut Vec<syn::Error>,
) -> proc_macro2::TokenStream {
let mut supported_file_types_attr = None;
for attr in &input.attrs {
if let Some(list) = util::get_attr_list("lofty", attr) {
if let Some(NestedMeta::Meta(Meta::List(ml))) = list.nested.first() {
if ml
.path
.segments
.first()
.expect("path shouldn't be empty")
.ident == "supported_formats"
{
supported_file_types_attr = Some(ml.clone());
}
}
}
}
if supported_file_types_attr.is_none() {
bail!(
errors,
input.ident.span(),
"Tag has no #[lofty(supported_formats)] attribute"
);
}
let ident = &input.ident;
let supported_file_types_attr = supported_file_types_attr.unwrap();
let file_types_iter = supported_file_types_attr.nested.iter();
quote! {
impl #ident {
pub(crate) const SUPPORTED_FORMATS: &'static [lofty::FileType] = &[
#( lofty::FileType:: #file_types_iter ),*
];
}
}
}

View file

@ -3,6 +3,15 @@ use std::fmt::Display;
use proc_macro2::Span; use proc_macro2::Span;
use syn::{Attribute, Lit, Meta, MetaList, NestedMeta, Type}; use syn::{Attribute, Lit, Meta, MetaList, NestedMeta, Type};
macro_rules! bail {
($errors:ident, $span:expr, $msg:literal) => {
$errors.push(crate::util::err($span, $msg));
return proc_macro2::TokenStream::new();
};
}
pub(crate) use bail;
pub(crate) fn get_attr(name: &str, attrs: &[Attribute]) -> Option<proc_macro2::TokenStream> { pub(crate) fn get_attr(name: &str, attrs: &[Attribute]) -> Option<proc_macro2::TokenStream> {
for attr in attrs { for attr in attrs {
if let Some(list) = get_attr_list("lofty", attr) { if let Some(list) = get_attr_list("lofty", attr) {

View file

@ -13,6 +13,8 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
macro_rules! impl_accessor { macro_rules! impl_accessor {
($($name:ident => $($key:literal)|+;)+) => { ($($name:ident => $($key:literal)|+;)+) => {
paste::paste! { paste::paste! {
@ -47,7 +49,6 @@ macro_rules! impl_accessor {
} }
} }
#[derive(Default, Debug, PartialEq, Eq, Clone)]
/// An `APE` tag /// An `APE` tag
/// ///
/// ## Supported file types /// ## Supported file types
@ -71,6 +72,8 @@ macro_rules! impl_accessor {
/// ///
/// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded. /// When converting pictures, any of type [`PictureType::Undefined`](crate::PictureType::Undefined) will be discarded.
/// For items, see [`ApeItem::new`]. /// For items, see [`ApeItem::new`].
#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(APE, MPEG, WavPack))]
pub struct ApeTag { pub struct ApeTag {
/// Whether or not to mark the tag as read only /// Whether or not to mark the tag as read only
pub read_only: bool, pub read_only: bool,

View file

@ -4,7 +4,6 @@ use super::ApeTagRef;
use crate::ape::constants::APE_PREAMBLE; use crate::ape::constants::APE_PREAMBLE;
use crate::ape::header::read_ape_header; use crate::ape::header::read_ape_header;
use crate::error::Result; use crate::error::Result;
use crate::file::FileType;
use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2}; use crate::id3::{find_id3v1, find_id3v2, find_lyrics3v2};
use crate::macros::{decode_err, err}; use crate::macros::{decode_err, err};
use crate::probe::Probe; use crate::probe::Probe;
@ -23,7 +22,7 @@ where
let probe = Probe::new(data).guess_file_type()?; let probe = Probe::new(data).guess_file_type()?;
match probe.file_type() { match probe.file_type() {
Some(FileType::APE | FileType::MPEG | FileType::WavPack) => {}, Some(ft) if super::ApeTag::SUPPORTED_FORMATS.contains(&ft) => {},
_ => err!(UnsupportedTag), _ => err!(UnsupportedTag),
} }

View file

@ -8,6 +8,8 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
macro_rules! impl_accessor { macro_rules! impl_accessor {
($($name:ident,)+) => { ($($name:ident,)+) => {
paste::paste! { paste::paste! {
@ -52,7 +54,8 @@ macro_rules! impl_accessor {
/// ///
/// * [`GENRES`] contains the string /// * [`GENRES`] contains the string
/// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8` /// * The [`ItemValue`](crate::ItemValue) can be parsed into a `u8`
#[derive(Default, Debug, PartialEq, Eq, Clone)] #[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(APE, MPEG, WavPack))]
pub struct ID3v1Tag { pub struct ID3v1Tag {
/// Track title, 30 bytes max /// Track title, 30 bytes max
pub title: Option<String>, pub title: Option<String>,

View file

@ -1,6 +1,5 @@
use super::tag::Id3v1TagRef; use super::tag::Id3v1TagRef;
use crate::error::Result; use crate::error::Result;
use crate::file::FileType;
use crate::id3::{find_id3v1, ID3FindResults}; use crate::id3::{find_id3v1, ID3FindResults};
use crate::macros::err; use crate::macros::err;
use crate::probe::Probe; use crate::probe::Probe;
@ -15,7 +14,7 @@ pub(crate) fn write_id3v1(file: &mut File, tag: &Id3v1TagRef<'_>) -> Result<()>
let probe = Probe::new(file).guess_file_type()?; let probe = Probe::new(file).guess_file_type()?;
match probe.file_type() { match probe.file_type() {
Some(FileType::APE | FileType::MPEG | FileType::WavPack) => {}, Some(ft) if super::ID3v1Tag::SUPPORTED_FORMATS.contains(&ft) => {},
_ => err!(UnsupportedTag), _ => err!(UnsupportedTag),
} }

View file

@ -18,6 +18,8 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
macro_rules! impl_accessor { macro_rules! impl_accessor {
($($name:ident => $id:literal;)+) => { ($($name:ident => $id:literal;)+) => {
paste::paste! { paste::paste! {
@ -59,7 +61,6 @@ macro_rules! impl_accessor {
} }
} }
#[derive(PartialEq, Eq, Debug, Clone)]
/// An `ID3v2` tag /// An `ID3v2` tag
/// ///
/// ## Supported file types /// ## Supported file types
@ -97,6 +98,8 @@ macro_rules! impl_accessor {
/// and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively, and converted back to binary with /// and [`SynchronizedText::parse`](crate::id3::v2::SynchronizedText::parse) respectively, and converted back to binary with
/// [`GeneralEncapsulatedObject::as_bytes`](crate::id3::v2::GeneralEncapsulatedObject::as_bytes) and /// [`GeneralEncapsulatedObject::as_bytes`](crate::id3::v2::GeneralEncapsulatedObject::as_bytes) and
/// [`SynchronizedText::as_bytes`](crate::id3::v2::SynchronizedText::as_bytes) for writing. /// [`SynchronizedText::as_bytes`](crate::id3::v2::SynchronizedText::as_bytes) for writing.
#[derive(LoftyTag, PartialEq, Eq, Debug, Clone)]
#[lofty(supported_formats(APE, AIFF, FLAC, MPEG, WAV))]
pub struct ID3v2Tag { pub struct ID3v2Tag {
flags: ID3v2TagFlags, flags: ID3v2TagFlags,
pub(super) original_version: ID3v2Version, pub(super) original_version: ID3v2Version,

View file

@ -15,6 +15,7 @@ use std::fs::File;
use std::io::{Cursor, Read, Seek, SeekFrom, Write}; use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::ops::Not; use std::ops::Not;
use crate::id3::v2::ID3v2Tag;
use byteorder::{BigEndian, LittleEndian, WriteBytesExt}; use byteorder::{BigEndian, LittleEndian, WriteBytesExt};
// In the very rare chance someone wants to write a CRC in their extended header // In the very rare chance someone wants to write a CRC in their extended header
@ -41,8 +42,11 @@ pub(crate) fn write_id3v2<'a, I: Iterator<Item = FrameRef<'a>> + 'a>(
let data = probe.into_inner(); let data = probe.into_inner();
if file_type.is_none() || !ID3v2Tag::SUPPORTED_FORMATS.contains(&(file_type.unwrap())) {
err!(UnsupportedTag);
}
match file_type { match file_type {
Some(FileType::APE | FileType::MPEG | FileType::FLAC) => {},
// Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file // Formats such as WAV and AIFF store the ID3v2 tag in an 'ID3 ' chunk rather than at the beginning of the file
Some(FileType::WAV) => { Some(FileType::WAV) => {
tag.flags.footer = false; tag.flags.footer = false;
@ -52,7 +56,7 @@ pub(crate) fn write_id3v2<'a, I: Iterator<Item = FrameRef<'a>> + 'a>(
tag.flags.footer = false; tag.flags.footer = false;
return chunk_file::write_to_chunk_file::<BigEndian>(data, &create_tag(tag)?); return chunk_file::write_to_chunk_file::<BigEndian>(data, &create_tag(tag)?);
}, },
_ => err!(UnsupportedTag), _ => {},
} }
let id3v2 = create_tag(tag)?; let id3v2 = create_tag(tag)?;

View file

@ -11,6 +11,7 @@ use std::io::{Read, Seek, SeekFrom, Write};
use std::path::Path; use std::path::Path;
use byteorder::BigEndian; use byteorder::BigEndian;
use lofty_attr::LoftyTag;
/// Represents an AIFF `COMT` chunk /// Represents an AIFF `COMT` chunk
/// ///
@ -60,7 +61,8 @@ pub struct Comment {
/// * [`ItemKey::Comment`](crate::ItemKey::Comment) /// * [`ItemKey::Comment`](crate::ItemKey::Comment)
/// ///
/// When converting [Comment]s, only the `text` field will be preserved. /// When converting [Comment]s, only the `text` field will be preserved.
#[derive(Default, Clone, Debug, PartialEq, Eq)] #[derive(LoftyTag, Default, Clone, Debug, PartialEq, Eq)]
#[lofty(supported_formats(AIFF))]
pub struct AIFFTextChunks { pub struct AIFFTextChunks {
/// The name of the piece /// The name of the piece
pub name: Option<String>, pub name: Option<String>,

View file

@ -10,6 +10,8 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
macro_rules! impl_accessor { macro_rules! impl_accessor {
($($name:ident => $key:literal;)+) => { ($($name:ident => $key:literal;)+) => {
paste::paste! { paste::paste! {
@ -30,7 +32,6 @@ macro_rules! impl_accessor {
} }
} }
#[derive(Default, Debug, PartialEq, Eq, Clone)]
/// A RIFF INFO LIST /// A RIFF INFO LIST
/// ///
/// ## Supported file types /// ## Supported file types
@ -45,6 +46,8 @@ macro_rules! impl_accessor {
/// ///
/// * The [`TagItem`] has a value other than [`ItemValue::Binary`](crate::ItemValue::Binary) /// * The [`TagItem`] has a value other than [`ItemValue::Binary`](crate::ItemValue::Binary)
/// * It has a key that is 4 bytes in length and within the ASCII range /// * It has a key that is 4 bytes in length and within the ASCII range
#[derive(LoftyTag, Default, Debug, PartialEq, Eq, Clone)]
#[lofty(supported_formats(WAV))]
pub struct RIFFInfoList { pub struct RIFFInfoList {
/// A collection of chunk-value pairs /// A collection of chunk-value pairs
pub(crate) items: Vec<(String, String)>, pub(crate) items: Vec<(String, String)>,

View file

@ -18,6 +18,8 @@ use std::fs::{File, OpenOptions};
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
const ARTIST: AtomIdent = AtomIdent::Fourcc(*b"\xa9ART"); const ARTIST: AtomIdent = AtomIdent::Fourcc(*b"\xa9ART");
const TITLE: AtomIdent = AtomIdent::Fourcc(*b"\xa9nam"); const TITLE: AtomIdent = AtomIdent::Fourcc(*b"\xa9nam");
const ALBUM: AtomIdent = AtomIdent::Fourcc(*b"\xa9alb"); const ALBUM: AtomIdent = AtomIdent::Fourcc(*b"\xa9alb");
@ -53,7 +55,6 @@ macro_rules! impl_accessor {
} }
} }
#[derive(Default, PartialEq, Debug, Clone)]
/// An MP4 ilst atom /// An MP4 ilst atom
/// ///
/// ## Supported file types /// ## Supported file types
@ -80,6 +81,8 @@ macro_rules! impl_accessor {
/// well as pictures, will be preserved. /// well as pictures, will be preserved.
/// ///
/// An attempt will be made to create the `TrackNumber/TrackTotal` (trkn) and `DiscNumber/DiscTotal` (disk) pairs. /// An attempt will be made to create the `TrackNumber/TrackTotal` (trkn) and `DiscNumber/DiscTotal` (disk) pairs.
#[derive(LoftyTag, Default, PartialEq, Debug, Clone)]
#[lofty(supported_formats(MP4))]
pub struct Ilst { pub struct Ilst {
pub(crate) atoms: Vec<Atom>, pub(crate) atoms: Vec<Atom>,
} }

View file

@ -13,6 +13,8 @@ use std::fs::{File, OpenOptions};
use std::io::{Cursor, Write}; use std::io::{Cursor, Write};
use std::path::Path; use std::path::Path;
use lofty_attr::LoftyTag;
macro_rules! impl_accessor { macro_rules! impl_accessor {
($($name:ident => $key:literal;)+) => { ($($name:ident => $key:literal;)+) => {
paste::paste! { paste::paste! {
@ -41,7 +43,8 @@ macro_rules! impl_accessor {
/// * [`FileType::Opus`](crate::FileType::Opus) /// * [`FileType::Opus`](crate::FileType::Opus)
/// * [`FileType::Speex`](crate::FileType::Speex) /// * [`FileType::Speex`](crate::FileType::Speex)
/// * [`FileType::Vorbis`](crate::FileType::Vorbis) /// * [`FileType::Vorbis`](crate::FileType::Vorbis)
#[derive(Default, PartialEq, Eq, Debug, Clone)] #[derive(LoftyTag, Default, PartialEq, Eq, Debug, Clone)]
#[lofty(supported_formats(FLAC, Opus, Speex, Vorbis))]
pub struct VorbisComments { pub struct VorbisComments {
/// An identifier for the encoding software /// An identifier for the encoding software
pub(crate) vendor: String, pub(crate) vendor: String,