use crate::error::Result;
use crate::file::{AudioFile, FileType, TaggedFile};
use crate::probe::ParseOptions;
use crate::tag::TagType;
use std::collections::HashMap;
use std::io::{Read, Seek};
use std::marker::PhantomData;
use std::sync::{Arc, Mutex};
use once_cell::sync::Lazy;
pub trait FileResolver: Send + Sync + AudioFile {
fn extension() -> Option<&'static str>;
fn primary_tag_type() -> TagType;
fn supported_tag_types() -> &'static [TagType];
fn guess(buf: &[u8]) -> Option<FileType>;
}
type ResolverMap = HashMap<&'static str, &'static dyn ObjectSafeFileResolver>;
pub(crate) static CUSTOM_RESOLVERS: Lazy<Arc<Mutex<ResolverMap>>> =
Lazy::new(|| Arc::new(Mutex::new(HashMap::new())));
pub(crate) fn lookup_resolver(name: &'static str) -> &'static dyn ObjectSafeFileResolver {
let res = CUSTOM_RESOLVERS.lock().unwrap();
if let Some(resolver) = res.get(name).copied() {
return resolver;
}
panic!(
"Encountered an unregistered custom `FileType` named `{}`",
name
);
}
pub(crate) trait SeekRead: Read + Seek {}
impl<T: Seek + Read> SeekRead for T {}
pub(crate) trait ObjectSafeFileResolver: Send + Sync {
fn extension(&self) -> Option<&'static str>;
fn primary_tag_type(&self) -> TagType;
fn supported_tag_types(&self) -> &'static [TagType];
fn guess(&self, buf: &[u8]) -> Option<FileType>;
fn read_from(
&self,
reader: &mut dyn SeekRead,
parse_options: ParseOptions,
) -> Result<TaggedFile>;
}
pub(crate) struct GhostlyResolver<T: 'static>(PhantomData<T>);
impl<T: FileResolver> ObjectSafeFileResolver for GhostlyResolver<T> {
fn extension(&self) -> Option<&'static str> {
T::extension()
}
fn primary_tag_type(&self) -> TagType {
T::primary_tag_type()
}
fn supported_tag_types(&self) -> &'static [TagType] {
T::supported_tag_types()
}
fn guess(&self, buf: &[u8]) -> Option<FileType> {
T::guess(buf)
}
fn read_from(
&self,
reader: &mut dyn SeekRead,
parse_options: ParseOptions,
) -> Result<TaggedFile> {
Ok(<T as AudioFile>::read_from(&mut Box::new(reader), parse_options)?.into())
}
}
pub fn register_custom_resolver<T: FileResolver + 'static>(name: &'static str) {
let mut res = CUSTOM_RESOLVERS.lock().unwrap();
assert!(
res.iter().all(|(n, _)| *n != name),
"Resolver `{}` already exists!",
name
);
let ghost = GhostlyResolver::<T>(PhantomData);
let b: Box<dyn ObjectSafeFileResolver> = Box::new(ghost);
res.insert(name, Box::leak::<'static>(b));
}
#[cfg(test)]
mod tests {
use crate::file::{FileType, TaggedFileExt};
use crate::id3::v2::Id3v2Tag;
use crate::probe::ParseOptions;
use crate::properties::FileProperties;
use crate::resolve::{register_custom_resolver, FileResolver};
use crate::tag::TagType;
use crate::traits::Accessor;
use std::fs::File;
use std::io::{Read, Seek};
use std::panic;
use lofty_attr::LoftyFile;
#[derive(LoftyFile, Default)]
#[lofty(read_fn = "Self::read")]
#[lofty(file_type = "MyFile")]
struct MyFile {
#[lofty(tag_type = "Id3v2")]
id3v2_tag: Option<Id3v2Tag>,
properties: FileProperties,
}
impl FileResolver for MyFile {
fn extension() -> Option<&'static str> {
Some("myfile")
}
fn primary_tag_type() -> TagType {
TagType::Id3v2
}
fn supported_tag_types() -> &'static [TagType] {
&[TagType::Id3v2]
}
fn guess(buf: &[u8]) -> Option<FileType> {
if buf.starts_with(b"myfile") {
return Some(FileType::Custom("MyFile"));
}
None
}
}
impl MyFile {
#[allow(clippy::unnecessary_wraps)]
fn read<R: Read + Seek + ?Sized>(
_reader: &mut R,
_parse_options: ParseOptions,
) -> crate::error::Result<Self> {
let mut tag = Id3v2Tag::default();
tag.set_artist(String::from("All is well!"));
Ok(Self {
id3v2_tag: Some(tag),
properties: FileProperties::default(),
})
}
}
#[test]
fn custom_resolver() {
register_custom_resolver::<MyFile>("MyFile");
let path = "examples/custom_resolver/test_asset.myfile";
let read = crate::read_from_path(path).unwrap();
assert_eq!(read.file_type(), FileType::Custom("MyFile"));
let read_content = crate::read_from(&mut File::open(path).unwrap()).unwrap();
assert_eq!(read_content.file_type(), FileType::Custom("MyFile"));
assert!(
panic::catch_unwind(|| {
register_custom_resolver::<MyFile>("MyFile");
})
.is_err(),
"We didn't panic on double register!"
);
}
}