Add the ability to guess file format from file signature

Adds a new DetermineFrom enum to be used in Tag::read_from_path. This allows you to choose between guessing from extension or file signature.
This commit is contained in:
Serial 2021-04-20 23:06:03 -04:00
parent baa1b1fd24
commit 253609cc4b
9 changed files with 107 additions and 27 deletions

View file

@ -46,7 +46,7 @@ impl<'a> From<&'a ApeTag> for AnyTag<'a> {
comments: None,
date: None, // TODO
#[cfg(feature = "duration")]
duration: None,
duration: inp.duration,
}
}
}

View file

@ -52,7 +52,7 @@ impl<'a> From<&'a Mp4Tag> for AnyTag<'a> {
comments: None,
date: None,
#[cfg(feature = "duration")]
duration: None, // TODO?
duration: inp.duration, // TODO?
}
}
}

View file

@ -179,7 +179,9 @@ impl<'a> From<&'a VorbisTag> for AnyTag<'a> {
total_tracks: inp.total_tracks(),
disc_number: inp.disc_number(),
total_discs: inp.total_discs(),
..AnyTag::default()
comments: None,
date: None,
duration: inp.duration
}
}
}

View file

@ -92,7 +92,9 @@ impl<'a> From<&'a WavTag> for AnyTag<'a> {
total_tracks: inp.total_tracks(),
disc_number: inp.disc_number(),
total_discs: inp.total_discs(),
..AnyTag::default()
comments: None,
date: None,
duration: inp.duration
}
}
}

View file

@ -5,6 +5,11 @@ pub enum Error {
#[error("Failed to guess the metadata format based on the file extension.")]
UnknownFileExtension,
#[error("No format could be determined from the provided file.")]
UnknownFormat,
#[error("File contains no data")]
EmptyFile,
/// Unsupported file extension
#[error("Unsupported format: {0}")]
UnsupportedFormat(String),

View file

@ -29,10 +29,10 @@
//! # Examples
//!
//! ```
//! use lofty::{Tag, TagType};
//! use lofty::{Tag, TagType, DetermineFrom};
//!
//! // Guess the format from the extension, in this case `mp3`
//! let mut tag = Tag::new().read_from_path("tests/assets/a.mp3").unwrap();
//! let mut tag = Tag::new().read_from_path("tests/assets/a.mp3", DetermineFrom::Extension).unwrap();
//! tag.set_title("Foo");
//!
//! // You can convert the tag type and save the metadata to another file.
@ -40,7 +40,7 @@
//!
//! // You can specify the tag type, but when you want to do this
//! // also consider directly using the concrete type
//! let tag = Tag::new().with_tag_type(TagType::Mp4).read_from_path("tests/assets/a.m4a").unwrap();
//! let tag = Tag::new().with_tag_type(TagType::Mp4).read_from_path("tests/assets/a.m4a", DetermineFrom::Extension).unwrap();
//! assert_eq!(tag.title(), Some("Foo"));
//! ```
//!
@ -95,7 +95,7 @@ pub use crate::types::{
};
mod tag;
pub use crate::tag::{Tag, TagType};
pub use crate::tag::{DetermineFrom, Tag, TagType};
mod error;
pub use crate::error::{Error, Result};

View file

@ -3,10 +3,35 @@ use super::{components::tags::*, AudioTag, Error, Result};
use crate::{Id3v2Tag, WavTag};
use std::path::Path;
#[cfg(feature = "ape")]
const MAC: [u8; 3] = [77, 65, 67];
#[cfg(feature = "mp3")]
const ID3: [u8; 3] = [73, 68, 51];
#[cfg(feature = "mp4")]
const FTYP: [u8; 4] = [102, 116, 121, 112];
#[cfg(feature = "vorbis")]
const OPUSHEAD: [u8; 8] = [79, 112, 117, 115, 72, 101, 97, 100];
#[cfg(feature = "vorbis")]
const FLAC: [u8; 4] = [102, 76, 97, 67];
#[cfg(feature = "vorbis")]
const OGGS: [u8; 4] = [79, 103, 103, 83];
#[cfg(feature = "vorbis")]
const VORBIS: [u8; 6] = [118, 111, 114, 98, 105, 115];
#[cfg(feature = "wav")]
const RIFF: [u8; 4] = [82, 73, 70, 70];
/// A builder for `Box<dyn AudioTag>`. If you do not want a trait object, you can use individual types.
#[derive(Default)]
pub struct Tag(Option<TagType>);
/// Used in Tag::read_from_path to choose the method to determine the tag type
pub enum DetermineFrom {
/// Determine the format from the file extension
Extension,
/// Determine the format by reading the file, and matching the signature
Signature,
}
impl Tag {
/// Initiate a new Tag
pub fn new() -> Self {
@ -19,24 +44,34 @@ impl Tag {
Self(Some(tag_type))
}
/// Path of the file to read
/// Path of the file to read, and the method to determine the tag type
///
/// # Errors
///
/// This function will return `Err` if `path` either has no extension, or the extension is
/// not valid unicode
pub fn read_from_path(&self, path: impl AsRef<Path>) -> Result<Box<dyn AudioTag>> {
let extension = path
.as_ref()
.extension()
.ok_or(Error::UnknownFileExtension)?;
let extension_str = extension.to_str().ok_or(Error::UnknownFileExtension)?;
/// * `path` either has no extension, or the extension is not valid unicode (DetermineFrom::Extension)
/// * `path` has an unsupported/unknown extension (DetermineFrom::Extension)
/// * `path` does not exist (DetermineFrom::Signature)
pub fn read_from_path(
&self,
path: impl AsRef<Path>,
method: DetermineFrom,
) -> Result<Box<dyn AudioTag>> {
let tag_type = match method {
DetermineFrom::Extension => {
let extension = path
.as_ref()
.extension()
.ok_or(Error::UnknownFileExtension)?;
let extension_str = extension.to_str().ok_or(Error::UnknownFileExtension)?;
match self
.0
.as_ref()
.unwrap_or(&TagType::try_from_ext(extension_str)?)
{
TagType::try_from_ext(extension_str)?
},
DetermineFrom::Signature => {
TagType::try_from_sig(&std::fs::read(path.as_ref())?[0..36])?
},
};
match tag_type {
#[cfg(feature = "ape")]
TagType::Ape => Ok(Box::new(ApeTag::read_from_path(path)?)),
#[cfg(feature = "mp3")]
@ -105,4 +140,36 @@ impl TagType {
_ => Err(Error::UnsupportedFormat(ext.to_owned())),
}
}
fn try_from_sig(data: &[u8]) -> Result<Self> {
if data.len() < 1 {
return Err(Error::EmptyFile);
}
match data[0] {
#[cfg(feature = "ape")]
77 if data.starts_with(&MAC) => Ok(Self::Ape),
#[cfg(feature = "mp3")]
73 if data.starts_with(&ID3) => Ok(Self::Id3v2),
#[cfg(feature = "mp4")]
#[cfg(feature = "vorbis")]
102 if data.starts_with(&FLAC) => Ok(Self::Vorbis(VorbisFormat::Flac)),
#[cfg(feature = "vorbis")]
79 if data.starts_with(&OGGS) => {
if data[29..35] == VORBIS {
return Ok(Self::Vorbis(VorbisFormat::Ogg));
}
if data[28..36] == OPUSHEAD {
return Ok(Self::Vorbis(VorbisFormat::Opus));
}
Err(Error::UnknownFormat)
},
#[cfg(feature = "wav")]
82 if data.starts_with(&RIFF) => Ok(Self::Wav),
#[cfg(feature = "mp4")]
_ if data[4..8] == FTYP => Ok(Self::Mp4),
_ => Err(Error::UnknownFormat),
}
}
}

View file

@ -1,4 +1,4 @@
use lofty::{Tag, TagType, ToAnyTag, VorbisTag};
use lofty::{DetermineFrom, Tag, TagType, ToAnyTag, VorbisTag};
#[test]
#[cfg(all(feature = "mp3", feature = "vorbis"))]
@ -24,7 +24,7 @@ fn test_inner() {
// Read from `a.mp3`
let id3tag_reload = Tag::default()
.read_from_path("tests/assets/a.mp3")
.read_from_path("tests/assets/a.mp3", DetermineFrom::Extension)
.expect("Fail to read!");
// Confirm title still matches

View file

@ -1,5 +1,5 @@
#![cfg(feature = "default")]
use lofty::{MimeType, Picture, Tag};
use lofty::{DetermineFrom, MimeType, Picture, Tag};
macro_rules! full_test {
($function:ident, $file:expr) => {
@ -16,7 +16,9 @@ macro_rules! full_test {
macro_rules! add_tags {
($file:expr) => {
println!("Reading file");
let mut tag = Tag::default().read_from_path($file).unwrap();
let mut tag = Tag::default()
.read_from_path($file, DetermineFrom::Signature)
.unwrap();
println!("Setting title");
tag.set_title("foo title");
@ -55,7 +57,9 @@ macro_rules! add_tags {
macro_rules! remove_tags {
($file:expr) => {
println!("Reading file");
let mut tag = Tag::default().read_from_path($file).unwrap();
let mut tag = Tag::default()
.read_from_path($file, DetermineFrom::Extension)
.unwrap();
println!("Checking title");
assert_eq!(tag.title(), Some("foo title"));