mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 14:44:22 +00:00
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:
parent
baa1b1fd24
commit
253609cc4b
9 changed files with 107 additions and 27 deletions
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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};
|
||||
|
|
95
src/tag.rs
95
src/tag.rs
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
10
tests/io.rs
10
tests/io.rs
|
@ -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"));
|
||||
|
|
Loading…
Reference in a new issue