use crate::config::global_options;
use crate::resolve::custom_resolvers;
use crate::tag::{TagSupport, TagType};
use std::ffi::OsStr;
use std::path::Path;
pub const EXTENSIONS: &[&str] = &[
"aac", "ape", "aiff", "aif", "afc", "aifc", "mp3", "mp2", "mp1", "wav", "wv", "opus", "flac",
"ogg", "mp4", "m4a", "m4b", "m4p", "m4r", "m4v", "3gp", "mpc", "mp+", "mpp", "spx",
];
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
#[allow(clippy::unsafe_derive_deserialize)]
#[non_exhaustive]
pub enum FileType {
Aac,
Aiff,
Ape,
Flac,
Mpeg,
Mp4,
Mpc,
Opus,
Vorbis,
Speex,
Wav,
WavPack,
Custom(&'static str),
}
impl FileType {
pub fn primary_tag_type(&self) -> TagType {
match self {
FileType::Aac | FileType::Aiff | FileType::Mpeg | FileType::Wav => TagType::Id3v2,
FileType::Ape | FileType::Mpc | 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 tag_support(&self, tag_type: TagType) -> TagSupport {
if let FileType::Custom(c) = self {
let resolver = crate::resolve::lookup_resolver(c);
return resolver.tag_support(tag_type);
}
macro_rules! tag_support {
(
$tag_type:ident,
$(($variant:ident, $tag:path)),* $(,)?
) => {
match $tag_type {
$(
TagType::$variant => {
if <$tag>::SUPPORTED_FORMATS.contains(self) {
if <$tag>::READ_ONLY_FORMATS.contains(self) {
return TagSupport::ReadOnly;
}
return TagSupport::ReadWrite;
}
TagSupport::Unsupported
},
)*
}
}
}
tag_support!(
tag_type,
(Ape, crate::ape::ApeTag),
(Id3v1, crate::id3::v1::Id3v1Tag),
(Id3v2, crate::id3::v2::Id3v2Tag),
(Mp4Ilst, crate::mp4::Ilst),
(VorbisComments, crate::ogg::VorbisComments),
(RiffInfo, crate::iff::wav::RiffInfoList),
(AiffText, crate::iff::aiff::AiffTextChunks),
)
}
pub fn from_ext<E>(ext: E) -> Option<Self>
where
E: AsRef<OsStr>,
{
let ext = ext.as_ref().to_str()?.to_ascii_lowercase();
if unsafe { global_options().use_custom_resolvers } {
if let Some((ty, _)) = custom_resolvers()
.lock()
.ok()?
.iter()
.find(|(_, f)| f.extension() == Some(ext.as_str()))
{
return Some(Self::Custom(ty));
}
}
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),
"mpc" | "mp+" | "mpp" => Some(Self::Mpc),
"spx" => Some(Self::Speex),
_ => 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(FileTypeGuessResult::Determined(file_ty)) => Some(file_ty),
_ => None,
}
}
pub(crate) fn from_buffer_inner(buf: &[u8]) -> Option<FileTypeGuessResult> {
use crate::id3::v2::util::synchsafe::SynchsafeInteger;
let mut ret = None;
if buf.is_empty() {
return ret;
}
match Self::quick_type_guess(buf) {
Some(f_ty) => ret = Some(FileTypeGuessResult::Determined(f_ty)),
None if buf.len() >= 10 && &buf[..3] == b"ID3" => {
if let Ok(arr) = buf[6..10].try_into() {
ret = Some(FileTypeGuessResult::MaybePrecededById3(
u32::from_be_bytes(arr).unsynch(),
));
}
},
None => ret = Some(FileTypeGuessResult::MaybePrecededByJunk),
}
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),
_ if buf.starts_with(b"MPCK") || buf.starts_with(b"MP+") => Some(Self::Mpc),
_ => None,
}
}
}
pub(crate) enum FileTypeGuessResult {
Determined(FileType),
MaybePrecededById3(u32),
MaybePrecededByJunk,
}