use crate::error::Result;
use crate::probe::ParseOptions;
use crate::properties::FileProperties;
use crate::resolve::CUSTOM_RESOLVERS;
use crate::tag::{Tag, TagType};
use crate::traits::TagExt;
use std::convert::TryInto;
use std::ffi::OsStr;
use std::fs::{File, OpenOptions};
use std::io::{Read, Seek};
use std::path::Path;
pub trait AudioFile: Into<TaggedFile> {
type Properties;
fn read_from<R>(reader: &mut R, parse_options: ParseOptions) -> Result<Self>
where
R: Read + Seek,
Self: Sized;
fn save_to_path(&self, path: impl AsRef<Path>) -> Result<()> {
self.save_to(&mut OpenOptions::new().read(true).write(true).open(path)?)
}
fn save_to(&self, file: &mut File) -> Result<()>;
fn properties(&self) -> &Self::Properties;
fn contains_tag(&self) -> bool;
fn contains_tag_type(&self, tag_type: TagType) -> bool;
}
pub trait TaggedFileExt {
fn file_type(&self) -> FileType;
fn tags(&self) -> &[Tag];
fn primary_tag_type(&self) -> TagType;
fn supports_tag_type(&self, tag_type: TagType) -> bool;
fn tag(&self, tag_type: TagType) -> Option<&Tag>;
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag>;
fn primary_tag(&self) -> Option<&Tag>;
fn primary_tag_mut(&mut self) -> Option<&mut Tag>;
fn first_tag(&self) -> Option<&Tag>;
fn first_tag_mut(&mut self) -> Option<&mut Tag>;
fn insert_tag(&mut self, tag: Tag) -> Option<Tag>;
fn remove(&mut self, tag_type: TagType) -> Option<Tag>;
fn clear(&mut self);
}
pub struct TaggedFile {
pub(crate) ty: FileType,
pub(crate) properties: FileProperties,
pub(crate) tags: Vec<Tag>,
}
impl TaggedFile {
#[doc(hidden)]
#[must_use]
pub const fn new(ty: FileType, properties: FileProperties, tags: Vec<Tag>) -> Self {
Self {
ty,
properties,
tags,
}
}
pub fn change_file_type(&mut self, file_type: FileType) {
self.ty = file_type;
self.properties = FileProperties::default();
self.tags
.retain(|t| self.ty.supports_tag_type(t.tag_type()));
}
}
impl TaggedFileExt for TaggedFile {
fn file_type(&self) -> FileType {
self.ty
}
fn tags(&self) -> &[Tag] {
self.tags.as_slice()
}
fn primary_tag_type(&self) -> TagType {
self.ty.primary_tag_type()
}
fn supports_tag_type(&self, tag_type: TagType) -> bool {
self.ty.supports_tag_type(tag_type)
}
fn tag(&self, tag_type: TagType) -> Option<&Tag> {
self.tags.iter().find(|i| i.tag_type() == tag_type)
}
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
self.tags.iter_mut().find(|i| i.tag_type() == tag_type)
}
fn primary_tag(&self) -> Option<&Tag> {
self.tag(self.primary_tag_type())
}
fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
self.tag_mut(self.primary_tag_type())
}
fn first_tag(&self) -> Option<&Tag> {
self.tags.first()
}
fn first_tag_mut(&mut self) -> Option<&mut Tag> {
self.tags.first_mut()
}
fn insert_tag(&mut self, tag: Tag) -> Option<Tag> {
let tag_type = tag.tag_type();
if self.supports_tag_type(tag_type) {
let ret = self.remove(tag_type);
self.tags.push(tag);
return ret;
}
None
}
fn remove(&mut self, tag_type: TagType) -> Option<Tag> {
self.tags
.iter()
.position(|t| t.tag_type() == tag_type)
.map(|pos| self.tags.remove(pos))
}
fn clear(&mut self) {
self.tags.clear()
}
}
impl AudioFile for TaggedFile {
type Properties = FileProperties;
fn read_from<R>(reader: &mut R, parse_options: ParseOptions) -> Result<Self>
where
R: Read + Seek,
Self: Sized,
{
crate::probe::Probe::new(reader)
.guess_file_type()?
.options(parse_options)
.read()
}
fn save_to(&self, file: &mut File) -> Result<()> {
for tag in &self.tags {
file.rewind()?;
tag.save_to(file)?;
}
Ok(())
}
fn properties(&self) -> &Self::Properties {
&self.properties
}
fn contains_tag(&self) -> bool {
!self.tags.is_empty()
}
fn contains_tag_type(&self, tag_type: TagType) -> bool {
self.tags.iter().any(|t| t.tag_type() == tag_type)
}
}
impl From<BoundTaggedFile> for TaggedFile {
fn from(input: BoundTaggedFile) -> Self {
input.inner
}
}
pub struct BoundTaggedFile {
inner: TaggedFile,
file_handle: File,
}
impl BoundTaggedFile {
pub fn read_from(mut file: File, parse_options: ParseOptions) -> Result<Self> {
let inner = TaggedFile::read_from(&mut file, parse_options)?;
file.rewind()?;
Ok(Self {
inner,
file_handle: file,
})
}
pub fn save(&mut self) -> Result<()> {
self.inner.save_to(&mut self.file_handle)?;
self.inner.tags.retain(|tag| !tag.is_empty());
Ok(())
}
}
impl TaggedFileExt for BoundTaggedFile {
fn file_type(&self) -> FileType {
self.inner.file_type()
}
fn tags(&self) -> &[Tag] {
self.inner.tags()
}
fn primary_tag_type(&self) -> TagType {
self.inner.primary_tag_type()
}
fn supports_tag_type(&self, tag_type: TagType) -> bool {
self.inner.supports_tag_type(tag_type)
}
fn tag(&self, tag_type: TagType) -> Option<&Tag> {
self.inner.tag(tag_type)
}
fn tag_mut(&mut self, tag_type: TagType) -> Option<&mut Tag> {
self.inner.tag_mut(tag_type)
}
fn primary_tag(&self) -> Option<&Tag> {
self.inner.primary_tag()
}
fn primary_tag_mut(&mut self) -> Option<&mut Tag> {
self.inner.primary_tag_mut()
}
fn first_tag(&self) -> Option<&Tag> {
self.inner.first_tag()
}
fn first_tag_mut(&mut self) -> Option<&mut Tag> {
self.inner.first_tag_mut()
}
fn insert_tag(&mut self, tag: Tag) -> Option<Tag> {
self.inner.insert_tag(tag)
}
fn remove(&mut self, tag_type: TagType) -> Option<Tag> {
self.inner.remove(tag_type)
}
fn clear(&mut self) {
self.inner.clear()
}
}
impl AudioFile for BoundTaggedFile {
type Properties = FileProperties;
fn read_from<R>(_: &mut R, _: ParseOptions) -> Result<Self>
where
R: Read + Seek,
Self: Sized,
{
unimplemented!(
"BoundTaggedFile can only be constructed through `BoundTaggedFile::read_from`"
)
}
fn save_to(&self, file: &mut File) -> Result<()> {
self.inner.save_to(file)
}
fn properties(&self) -> &Self::Properties {
self.inner.properties()
}
fn contains_tag(&self) -> bool {
self.inner.contains_tag()
}
fn contains_tag_type(&self, tag_type: TagType) -> bool {
self.inner.contains_tag_type(tag_type)
}
}
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[allow(missing_docs)]
#[non_exhaustive]
pub enum FileType {
AAC,
AIFF,
APE,
FLAC,
MPEG,
MP4,
Opus,
Vorbis,
Speex,
WAV,
WavPack,
Custom(&'static str),
}
impl FileType {
pub fn primary_tag_type(&self) -> TagType {
match self {
FileType::AIFF | FileType::MPEG | FileType::WAV | FileType::AAC => TagType::ID3v2,
FileType::APE | FileType::WavPack => TagType::APE,
FileType::FLAC | FileType::Opus | FileType::Vorbis | FileType::Speex => {
TagType::VorbisComments
},
FileType::MP4 => TagType::MP4ilst,
FileType::Custom(c) => {
let resolver = crate::resolve::lookup_resolver(c);
resolver.primary_tag_type()
},
}
}
pub fn supports_tag_type(&self, tag_type: TagType) -> bool {
match self {
FileType::AIFF | FileType::APE | FileType::MPEG | FileType::WAV | FileType::AAC
if tag_type == TagType::ID3v2 =>
{
true
},
FileType::AIFF if tag_type == TagType::AIFFText => true,
FileType::APE | FileType::MPEG | FileType::WavPack | FileType::AAC
if tag_type == TagType::ID3v1 =>
{
true
},
FileType::APE | FileType::MPEG | FileType::WavPack if tag_type == TagType::APE => true,
FileType::Opus | FileType::FLAC | FileType::Vorbis | FileType::Speex => {
tag_type == TagType::VorbisComments
},
FileType::MP4 => tag_type == TagType::MP4ilst,
FileType::WAV => tag_type == TagType::RIFFInfo,
FileType::Custom(c) => {
let resolver = crate::resolve::lookup_resolver(c);
resolver.supported_tag_types().contains(&tag_type)
},
_ => false,
}
}
pub fn from_ext<E>(ext: E) -> Option<Self>
where
E: AsRef<OsStr>,
{
let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
match ext.as_str() {
"aac" => Some(Self::AAC),
"ape" => Some(Self::APE),
"aiff" | "aif" | "afc" | "aifc" => Some(Self::AIFF),
"mp3" | "mp2" | "mp1" => Some(Self::MPEG),
"wav" | "wave" => Some(Self::WAV),
"wv" => Some(Self::WavPack),
"opus" => Some(Self::Opus),
"flac" => Some(Self::FLAC),
"ogg" => Some(Self::Vorbis),
"mp4" | "m4a" | "m4b" | "m4p" | "m4r" | "m4v" | "3gp" => Some(Self::MP4),
"spx" => Some(Self::Speex),
e => {
if let Some((ty, _)) = CUSTOM_RESOLVERS
.lock()
.ok()?
.iter()
.find(|(_, f)| f.extension() == Some(e))
{
Some(Self::Custom(ty))
} else {
None
}
},
}
}
pub fn from_path<P>(path: P) -> Option<Self>
where
P: AsRef<Path>,
{
let ext = path.as_ref().extension();
ext.and_then(Self::from_ext)
}
pub fn from_buffer(buf: &[u8]) -> Option<Self> {
match Self::from_buffer_inner(buf) {
(Some(f_ty), _) => Some(f_ty),
_ => None,
}
}
pub(crate) fn from_buffer_inner(buf: &[u8]) -> (Option<Self>, Option<u32>) {
use crate::id3::v2::util::unsynch_u32;
let mut ret = (None, None);
if buf.is_empty() {
return ret;
}
match Self::quick_type_guess(buf) {
Some(f_ty) => ret.0 = Some(f_ty),
None if buf.len() >= 10 && &buf[..3] == b"ID3" => {
if let Ok(arr) = buf[6..10].try_into() {
ret.1 = Some(unsynch_u32(u32::from_be_bytes(arr)));
}
},
_ => {},
}
ret
}
fn quick_type_guess(buf: &[u8]) -> Option<Self> {
use crate::mpeg::header::verify_frame_sync;
match buf[0] {
77 if buf.starts_with(b"MAC") => Some(Self::APE),
255 if buf.len() >= 2 && verify_frame_sync([buf[0], buf[1]]) => {
if buf[1] & 0b10000 > 0 && buf[1] & 0b110 == 0 {
return Some(Self::AAC);
}
Some(Self::MPEG)
},
70 if buf.len() >= 12 && &buf[..4] == b"FORM" => {
let id = &buf[8..12];
if id == b"AIFF" || id == b"AIFC" {
return Some(Self::AIFF);
}
None
},
79 if buf.len() >= 36 && &buf[..4] == b"OggS" => {
if &buf[29..35] == b"vorbis" {
return Some(Self::Vorbis);
} else if &buf[28..36] == b"OpusHead" {
return Some(Self::Opus);
} else if &buf[28..36] == b"Speex " {
return Some(Self::Speex);
}
None
},
102 if buf.starts_with(b"fLaC") => Some(Self::FLAC),
82 if buf.len() >= 12 && &buf[..4] == b"RIFF" => {
if &buf[8..12] == b"WAVE" {
return Some(Self::WAV);
}
None
},
119 if buf.len() >= 4 && &buf[..4] == b"wvpk" => Some(Self::WavPack),
_ if buf.len() >= 8 && &buf[4..8] == b"ftyp" => Some(Self::MP4),
_ => None,
}
}
}