use crate::tagmap::{ConversionReport, SkipReason, StandardField, TagMap};
use crate::tags::Tags;
use crate::util::AnyFileThing;
use crate::{AudexError, FileType, ReadSeek, ReadWriteSeek, Result, StreamInfo};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt;
use std::fs::File as StdFile;
use std::io::{Read, Seek};
use std::path::{Path, PathBuf};
use std::time::Duration;
#[cfg(feature = "async")]
use tokio::fs::File as TokioFile;
type ItemsFunc = fn(&dyn Any) -> Vec<(String, Vec<String>)>;
type PopFunc = fn(&mut dyn Any, &str) -> Result<Option<Vec<String>>>;
type PopOrFunc = fn(&mut dyn Any, &str, Vec<String>) -> Result<Vec<String>>;
type LoaderFn = fn(&Path) -> Result<DynamicFileType>;
type ReaderLoaderFn = fn(&mut dyn ReadSeek) -> Result<DynamicFileType>;
struct FormatDescriptor {
name: &'static str,
_extensions: &'static [&'static str],
score_fn: fn(&str, &[u8]) -> i32,
load_fn: LoaderFn,
load_from_reader_fn: Option<ReaderLoaderFn>,
}
impl FormatDescriptor {
fn score(&self, filename: &str, header: &[u8]) -> i32 {
(self.score_fn)(filename, header)
}
fn load(&self, path: &Path) -> Result<DynamicFileType> {
(self.load_fn)(path)
}
}
fn load_format<T: FileType + 'static>(path: &Path) -> Result<DynamicFileType> {
let file = T::load(path)?;
Ok(DynamicFileType::new(file, Some(path.to_path_buf())))
}
fn load_format_from_reader<T: FileType + 'static>(
reader: &mut dyn ReadSeek,
) -> Result<DynamicFileType> {
let file = T::load_from_reader(reader)?;
Ok(DynamicFileType::new(file, None))
}
const MP4_EXT: &[&str] = &[".mp4", ".m4a", ".m4b", ".m4p", ".m4v", ".3gp", ".3g2"];
const ASF_EXT: &[&str] = &[".wma", ".asf"];
const OGG_VORBIS_EXT: &[&str] = &[".ogg"];
const OGG_FLAC_EXT: &[&str] = &[".oggflac", ".oga"];
const OGG_OPUS_EXT: &[&str] = &[".opus"];
const OGG_SPEEX_EXT: &[&str] = &[".spx"];
const OGG_THEORA_EXT: &[&str] = &[".ogv"];
const FLAC_EXT: &[&str] = &[".flac"];
const AIFF_EXT: &[&str] = &[".aiff", ".aif"];
const WAVE_EXT: &[&str] = &[".wav"];
const DSDIFF_EXT: &[&str] = &[".dff", ".dst"];
const DSF_EXT: &[&str] = &[".dsf"];
const MONKEYS_EXT: &[&str] = &[".ape"];
const WAVPACK_EXT: &[&str] = &[".wv"];
const TAK_EXT: &[&str] = &[".tak"];
const TRUEAUDIO_EXT: &[&str] = &[".tta"];
const OPTIMFROG_EXT: &[&str] = &[".ofr", ".ofs"];
const MP3_EXT: &[&str] = &[".mp3", ".mp2", ".mpg", ".mpeg"];
const MUSEPACK_EXT: &[&str] = &[".mpc", ".mpp", ".mp+"];
const AAC_EXT: &[&str] = &[".aac", ".adts", ".adif"];
const AC3_EXT: &[&str] = &[".ac3", ".eac3"];
const SMF_EXT: &[&str] = &[".mid", ".midi"];
const FORMAT_REGISTRY: &[FormatDescriptor] = &[
FormatDescriptor {
name: "MP4",
_extensions: MP4_EXT,
score_fn: crate::mp4::MP4::score,
load_fn: load_format::<crate::mp4::MP4>,
load_from_reader_fn: Some(load_format_from_reader::<crate::mp4::MP4>),
},
FormatDescriptor {
name: "ASF",
_extensions: ASF_EXT,
score_fn: crate::asf::ASF::score,
load_fn: load_format::<crate::asf::ASF>,
load_from_reader_fn: Some(load_format_from_reader::<crate::asf::ASF>),
},
FormatDescriptor {
name: "OggVorbis",
_extensions: OGG_VORBIS_EXT,
score_fn: crate::oggvorbis::OggVorbis::score,
load_fn: load_format::<crate::oggvorbis::OggVorbis>,
load_from_reader_fn: Some(load_format_from_reader::<crate::oggvorbis::OggVorbis>),
},
FormatDescriptor {
name: "OggFlac",
_extensions: OGG_FLAC_EXT,
score_fn: crate::oggflac::OggFlac::score,
load_fn: load_format::<crate::oggflac::OggFlac>,
load_from_reader_fn: Some(load_format_from_reader::<crate::oggflac::OggFlac>),
},
FormatDescriptor {
name: "OggOpus",
_extensions: OGG_OPUS_EXT,
score_fn: crate::oggopus::OggOpus::score,
load_fn: load_format::<crate::oggopus::OggOpus>,
load_from_reader_fn: Some(load_format_from_reader::<crate::oggopus::OggOpus>),
},
FormatDescriptor {
name: "OggSpeex",
_extensions: OGG_SPEEX_EXT,
score_fn: crate::oggspeex::OggSpeex::score,
load_fn: load_format::<crate::oggspeex::OggSpeex>,
load_from_reader_fn: Some(load_format_from_reader::<crate::oggspeex::OggSpeex>),
},
FormatDescriptor {
name: "OggTheora",
_extensions: OGG_THEORA_EXT,
score_fn: crate::oggtheora::OggTheora::score,
load_fn: load_format::<crate::oggtheora::OggTheora>,
load_from_reader_fn: Some(load_format_from_reader::<crate::oggtheora::OggTheora>),
},
FormatDescriptor {
name: "FLAC",
_extensions: FLAC_EXT,
score_fn: crate::flac::FLAC::score,
load_fn: load_format::<crate::flac::FLAC>,
load_from_reader_fn: Some(load_format_from_reader::<crate::flac::FLAC>),
},
FormatDescriptor {
name: "AIFF",
_extensions: AIFF_EXT,
score_fn: crate::aiff::AIFF::score,
load_fn: load_format::<crate::aiff::AIFF>,
load_from_reader_fn: Some(load_format_from_reader::<crate::aiff::AIFF>),
},
FormatDescriptor {
name: "WAVE",
_extensions: WAVE_EXT,
score_fn: crate::wave::WAVE::score,
load_fn: load_format::<crate::wave::WAVE>,
load_from_reader_fn: Some(load_format_from_reader::<crate::wave::WAVE>),
},
FormatDescriptor {
name: "DSDIFF",
_extensions: DSDIFF_EXT,
score_fn: crate::dsdiff::DSDIFF::score,
load_fn: load_format::<crate::dsdiff::DSDIFF>,
load_from_reader_fn: Some(load_format_from_reader::<crate::dsdiff::DSDIFF>),
},
FormatDescriptor {
name: "DSF",
_extensions: DSF_EXT,
score_fn: crate::dsf::DSF::score,
load_fn: load_format::<crate::dsf::DSF>,
load_from_reader_fn: Some(load_format_from_reader::<crate::dsf::DSF>),
},
FormatDescriptor {
name: "MonkeysAudio",
_extensions: MONKEYS_EXT,
score_fn: crate::monkeysaudio::MonkeysAudio::score,
load_fn: load_format::<crate::monkeysaudio::MonkeysAudio>,
load_from_reader_fn: Some(load_format_from_reader::<crate::monkeysaudio::MonkeysAudio>),
},
FormatDescriptor {
name: "WavPack",
_extensions: WAVPACK_EXT,
score_fn: crate::wavpack::WavPack::score,
load_fn: load_format::<crate::wavpack::WavPack>,
load_from_reader_fn: Some(load_format_from_reader::<crate::wavpack::WavPack>),
},
FormatDescriptor {
name: "TAK",
_extensions: TAK_EXT,
score_fn: crate::tak::TAK::score,
load_fn: load_format::<crate::tak::TAK>,
load_from_reader_fn: Some(load_format_from_reader::<crate::tak::TAK>),
},
FormatDescriptor {
name: "TrueAudio",
_extensions: TRUEAUDIO_EXT,
score_fn: crate::trueaudio::TrueAudio::score,
load_fn: load_format::<crate::trueaudio::TrueAudio>,
load_from_reader_fn: Some(load_format_from_reader::<crate::trueaudio::TrueAudio>),
},
FormatDescriptor {
name: "OptimFROG",
_extensions: OPTIMFROG_EXT,
score_fn: crate::optimfrog::OptimFROG::score,
load_fn: load_format::<crate::optimfrog::OptimFROG>,
load_from_reader_fn: Some(load_format_from_reader::<crate::optimfrog::OptimFROG>),
},
FormatDescriptor {
name: "MP3",
_extensions: MP3_EXT,
score_fn: crate::mp3::MP3::score,
load_fn: load_format::<crate::mp3::MP3>,
load_from_reader_fn: Some(load_format_from_reader::<crate::mp3::MP3>),
},
FormatDescriptor {
name: "Musepack",
_extensions: MUSEPACK_EXT,
score_fn: crate::musepack::Musepack::score,
load_fn: load_format::<crate::musepack::Musepack>,
load_from_reader_fn: Some(load_format_from_reader::<crate::musepack::Musepack>),
},
FormatDescriptor {
name: "APEv2",
_extensions: &[],
score_fn: crate::apev2::APEv2::score,
load_fn: load_format::<crate::apev2::APEv2>,
load_from_reader_fn: Some(load_format_from_reader::<crate::apev2::APEv2>),
},
FormatDescriptor {
name: "ID3",
_extensions: &[],
score_fn: crate::id3::ID3FileType::score,
load_fn: load_format::<crate::id3::ID3>,
load_from_reader_fn: Some(load_format_from_reader::<crate::id3::ID3>),
},
FormatDescriptor {
name: "AAC",
_extensions: AAC_EXT,
score_fn: crate::aac::AAC::score,
load_fn: load_format::<crate::aac::AAC>,
load_from_reader_fn: Some(load_format_from_reader::<crate::aac::AAC>),
},
FormatDescriptor {
name: "AC3",
_extensions: AC3_EXT,
score_fn: crate::ac3::AC3::score,
load_fn: load_format::<crate::ac3::AC3>,
load_from_reader_fn: Some(load_format_from_reader::<crate::ac3::AC3>),
},
FormatDescriptor {
name: "SMF",
_extensions: SMF_EXT,
score_fn: crate::smf::SMF::score,
load_fn: load_format::<crate::smf::SMF>,
load_from_reader_fn: Some(load_format_from_reader::<crate::smf::SMF>),
},
];
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct DynamicStreamInfo {
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::duration_as_secs_f64")
)]
length: Option<Duration>,
bitrate: Option<u32>,
sample_rate: Option<u32>,
channels: Option<u16>,
bits_per_sample: Option<u16>,
}
impl DynamicStreamInfo {
pub fn from_stream_info<T: StreamInfo>(info: &T) -> Self {
Self {
length: info.length(),
bitrate: info.bitrate(),
sample_rate: info.sample_rate(),
channels: info.channels(),
bits_per_sample: info.bits_per_sample(),
}
}
}
impl StreamInfo for DynamicStreamInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
}
fn sample_rate(&self) -> Option<u32> {
self.sample_rate
}
fn channels(&self) -> Option<u16> {
self.channels
}
fn bits_per_sample(&self) -> Option<u16> {
self.bits_per_sample
}
}
pub struct DynamicFileType {
inner: Box<dyn Any>,
vtable: &'static DynamicFileVTable,
filename: Option<PathBuf>,
}
struct DynamicFileVTable {
save: fn(&mut dyn Any) -> Result<()>,
clear: fn(&mut dyn Any) -> Result<()>,
save_to_writer: fn(&mut dyn Any, &mut dyn ReadWriteSeek) -> Result<()>,
clear_writer: fn(&mut dyn Any, &mut dyn ReadWriteSeek) -> Result<()>,
save_to_path: fn(&mut dyn Any, &Path) -> Result<()>,
has_tags: fn(&dyn Any) -> bool,
tags_pprint: fn(&dyn Any) -> Option<String>,
info_pprint: fn(&dyn Any) -> String,
info: fn(&dyn Any) -> DynamicStreamInfo,
mime_types: fn() -> &'static [&'static str],
format_name: &'static str,
add_tags: fn(&mut dyn Any) -> Result<()>,
get: fn(&dyn Any, &str) -> Option<Vec<String>>,
set: fn(&mut dyn Any, &str, Vec<String>) -> Result<()>,
remove: fn(&mut dyn Any, &str) -> Result<()>,
keys: fn(&dyn Any) -> Vec<String>,
contains_key: fn(&dyn Any, &str) -> bool,
items: ItemsFunc,
len: fn(&dyn Any) -> usize,
is_empty: fn(&dyn Any) -> bool,
get_first: fn(&dyn Any, &str) -> Option<String>,
set_single: fn(&mut dyn Any, &str, String) -> Result<()>,
pop: PopFunc,
pop_or: PopOrFunc,
get_or: fn(&dyn Any, &str, Vec<String>) -> Vec<String>,
to_tag_map: fn(&dyn Any) -> TagMap,
apply_tag_map: fn(&mut dyn Any, &TagMap) -> Result<ConversionReport>,
}
unsafe impl Send for DynamicFileVTable {}
unsafe impl Sync for DynamicFileVTable {}
const _: () = {
fn _assert_send_sync<T: Send + Sync>() {}
fn _check() {
_assert_send_sync::<DynamicFileVTable>();
}
};
static VTABLE_CACHE: std::sync::RwLock<Option<HashMap<TypeId, &'static DynamicFileVTable>>> =
std::sync::RwLock::new(None);
fn vtable_for<T: FileType + 'static>() -> &'static DynamicFileVTable {
let type_id = TypeId::of::<T>();
{
let cache = VTABLE_CACHE.read().unwrap_or_else(|e| e.into_inner());
if let Some(map) = cache.as_ref() {
if let Some(&vtable) = map.get(&type_id) {
return vtable;
}
}
}
let mut cache = VTABLE_CACHE.write().unwrap_or_else(|e| e.into_inner());
let map = cache.get_or_insert_with(HashMap::new);
map.entry(type_id)
.or_insert_with(|| Box::leak(Box::new(create_vtable_for::<T>())))
}
fn create_vtable_for<T: FileType + 'static>() -> DynamicFileVTable {
DynamicFileVTable {
save: |any| {
let file = any
.downcast_mut::<T>()
.ok_or_else(|| AudexError::InvalidOperation("Type mismatch in save".to_string()))?;
file.save()
},
clear: |any| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in clear".to_string())
})?;
file.clear()
},
save_to_writer: |any, writer| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in save_to_writer".to_string())
})?;
file.save_to_writer(writer)
},
clear_writer: |any, writer| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in clear_writer".to_string())
})?;
file.clear_writer(writer)
},
save_to_path: |any, path| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in save_to_path".to_string())
})?;
file.save_to_path(path)
},
has_tags: |any| {
if let Some(file) = any.downcast_ref::<T>() {
file.tags().is_some() || !file.keys().is_empty()
} else {
false
}
},
tags_pprint: |any| {
any.downcast_ref::<T>()
.and_then(|file| file.tags())
.map(|t| t.pprint())
},
info_pprint: |any| {
any.downcast_ref::<T>()
.map(|file| file.info().pprint())
.unwrap_or_else(|| "<No stream information>".to_string())
},
info: |any| {
any.downcast_ref::<T>()
.map(|file| DynamicStreamInfo::from_stream_info(file.info()))
.unwrap_or_else(|| DynamicStreamInfo {
length: None,
bitrate: None,
sample_rate: None,
channels: None,
bits_per_sample: None,
})
},
mime_types: T::mime_types,
format_name: T::format_id(),
add_tags: |any| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in add_tags".to_string())
})?;
file.add_tags()
},
get: |any, key| any.downcast_ref::<T>().and_then(|file| file.get(key)),
set: |any, key, values| {
let file = any
.downcast_mut::<T>()
.ok_or_else(|| AudexError::InvalidOperation("Type mismatch in set".to_string()))?;
file.set(key, values)
},
remove: |any, key| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in remove".to_string())
})?;
file.remove(key)
},
keys: |any| {
any.downcast_ref::<T>()
.map(|file| file.keys())
.unwrap_or_default()
},
contains_key: |any, key| {
any.downcast_ref::<T>()
.map(|file| file.contains_key(key))
.unwrap_or(false)
},
items: |any| {
any.downcast_ref::<T>()
.map(|file| file.items())
.unwrap_or_default()
},
len: |any| any.downcast_ref::<T>().map(|file| file.len()).unwrap_or(0),
is_empty: |any| {
any.downcast_ref::<T>()
.map(|file| file.is_empty())
.unwrap_or(true)
},
get_first: |any, key| any.downcast_ref::<T>().and_then(|file| file.get_first(key)),
set_single: |any, key, value| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in set_single".to_string())
})?;
file.set_single(key, value)
},
pop: |any, key| {
let file = any
.downcast_mut::<T>()
.ok_or_else(|| AudexError::InvalidOperation("Type mismatch in pop".to_string()))?;
file.pop(key)
},
pop_or: |any, key, default| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in pop_or".to_string())
})?;
file.pop_or(key, default)
},
get_or: |any, key, default| {
any.downcast_ref::<T>()
.map(|file| file.get_or(key, default.clone()))
.unwrap_or(default)
},
to_tag_map: |any| {
any.downcast_ref::<T>()
.map(|file| crate::file::items_to_tag_map(file.items(), type_name_short::<T>()))
.unwrap_or_default()
},
apply_tag_map: |any, map| {
let file = any.downcast_mut::<T>().ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch in apply_tag_map".to_string())
})?;
crate::file::tag_map_to_items(file, map, type_name_short::<T>())
},
}
}
fn type_name_short<T: 'static>() -> &'static str {
let full = std::any::type_name::<T>();
full.rsplit("::").next().unwrap_or(full)
}
fn tag_system_for_format(format_name: &str) -> Option<crate::tagmap::normalize::TagSystem> {
use crate::tagmap::normalize::TagSystem;
match format_name {
"MP3" | "AIFF" | "WAVE" | "DSF" | "DSDIFF" | "ID3" | "EasyMP3" => Some(TagSystem::ID3v2),
"FLAC" | "OggVorbis" | "OggFlac" | "OggOpus" | "OggSpeex" | "OggTheora" => {
Some(TagSystem::VorbisComment)
}
"MP4" | "M4A" => Some(TagSystem::MP4),
"MonkeysAudio" | "WavPack" | "Musepack" | "TAK" | "OptimFROG" | "APEv2" => {
Some(TagSystem::APEv2)
}
"ASF" => Some(TagSystem::ASF),
"TrueAudio" => None,
_ => None,
}
}
fn detect_tag_system_from_keys(
items: &[(String, Vec<String>)],
) -> Option<crate::tagmap::normalize::TagSystem> {
use crate::tagmap::normalize::TagSystem;
if items.is_empty() {
return None;
}
const KNOWN_ID3V2_FRAMES: &[&str] = &[
"TIT2", "TALB", "TPE1", "TPE2", "TRCK", "TYER", "TDRC", "TCON", "COMM", "APIC", "TXXX",
"TPOS", "TCOM", "TENC", "TBPM", "TLAN", "TPUB", "TSRC", "TCOP", "TEXT", "TDRL", "USLT",
"WOAR", "WXXX",
];
let four_char_keys: Vec<&str> = items
.iter()
.filter(|(k, _)| {
k.len() == 4
&& k.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit())
})
.map(|(k, _)| k.as_str())
.collect();
let id3_score = four_char_keys.len();
if id3_score > items.len() / 2 {
let has_known_frame = four_char_keys
.iter()
.any(|k| KNOWN_ID3V2_FRAMES.contains(k));
if has_known_frame {
Some(TagSystem::ID3v2)
} else {
Some(TagSystem::APEv2)
}
} else {
Some(TagSystem::APEv2)
}
}
pub(crate) fn items_to_tag_map(items: Vec<(String, Vec<String>)>, format_name: &str) -> TagMap {
use crate::tagmap::mappings;
use crate::tagmap::normalize::{TagSystem, normalize_track_disc, resolve_id3_genre};
let mut map = TagMap::new();
let system = tag_system_for_format(format_name).or_else(|| detect_tag_system_from_keys(&items));
debug_event!(format = %format_name, item_count = items.len(), "extracting tags to TagMap");
for (key, values) in items {
if values.is_empty() {
continue;
}
let standard = match system {
Some(TagSystem::ID3v2) => mappings::id3_to_standard(&key),
Some(TagSystem::VorbisComment) => mappings::vorbis_to_standard(&key),
Some(TagSystem::MP4) => mappings::mp4_to_standard(&key),
Some(TagSystem::APEv2) => mappings::ape_to_standard(&key),
Some(TagSystem::ASF) => mappings::asf_to_standard(&key),
None => None,
};
if let Some(field) = standard {
match (&field, system) {
(StandardField::TrackNumber, Some(TagSystem::ID3v2))
| (StandardField::TrackNumber, Some(TagSystem::APEv2)) => {
if let Some(raw) = values.first() {
let (num, total) = normalize_track_disc(raw);
if let Some(n) = num {
map.set(StandardField::TrackNumber, vec![n]);
}
if let Some(t) = total {
map.set(StandardField::TotalTracks, vec![t]);
}
}
}
(StandardField::DiscNumber, Some(TagSystem::ID3v2))
| (StandardField::DiscNumber, Some(TagSystem::APEv2)) => {
if let Some(raw) = values.first() {
let (num, total) = normalize_track_disc(raw);
if let Some(n) = num {
map.set(StandardField::DiscNumber, vec![n]);
}
if let Some(t) = total {
map.set(StandardField::TotalDiscs, vec![t]);
}
}
}
(StandardField::Genre, Some(TagSystem::ID3v2)) => {
let resolved: Vec<String> =
values.iter().map(|v| resolve_id3_genre(v)).collect();
if resolved.len() > 1 {
map.set(field, vec![resolved.join(", ")]);
} else {
map.set(field, resolved);
}
}
(StandardField::Genre, _) => {
if values.len() > 1 {
map.set(field, vec![values.join(", ")]);
} else {
map.set(field, values);
}
}
(StandardField::TrackNumber, Some(TagSystem::MP4)) => {
if let Some(raw) = values.first() {
let (num, total) = normalize_track_disc(raw);
if let Some(n) = num {
map.set(StandardField::TrackNumber, vec![n]);
}
if let Some(t) = total {
map.set(StandardField::TotalTracks, vec![t]);
}
}
}
(StandardField::DiscNumber, Some(TagSystem::MP4)) => {
if let Some(raw) = values.first() {
let (num, total) = normalize_track_disc(raw);
if let Some(n) = num {
map.set(StandardField::DiscNumber, vec![n]);
}
if let Some(t) = total {
map.set(StandardField::TotalDiscs, vec![t]);
}
}
}
(StandardField::Publisher, _) => {
map.set(StandardField::Publisher, values.clone());
if map.get(&StandardField::Label).is_none() {
map.set(StandardField::Label, values);
}
}
(StandardField::Label, _) => {
map.set(StandardField::Label, values.clone());
if map.get(&StandardField::Publisher).is_none() {
map.set(StandardField::Publisher, values);
}
}
(StandardField::EncodedBy, _) => {
map.set(StandardField::EncodedBy, values.clone());
if map.get(&StandardField::Encoder).is_none() {
map.set(StandardField::Encoder, values);
}
}
(StandardField::Encoder, _) => {
map.set(StandardField::Encoder, values.clone());
if map.get(&StandardField::EncodedBy).is_none() {
map.set(StandardField::EncodedBy, values);
}
}
(StandardField::Date, _) => {
if let Some(existing) = map.get(&StandardField::Date) {
let new_len = values.first().map(|v| v.len()).unwrap_or(0);
let old_len = existing.first().map(|v| v.len()).unwrap_or(0);
if new_len > old_len {
map.set(field, values);
}
} else {
map.set(field, values);
}
}
_ => {
map.set(field, values);
}
}
} else {
let prefix = match system {
Some(TagSystem::ID3v2) => "id3",
Some(TagSystem::VorbisComment) => "vorbis",
Some(TagSystem::MP4) => "mp4",
Some(TagSystem::APEv2) => "ape",
Some(TagSystem::ASF) => "asf",
None => "unknown",
};
map.set_custom(format!("{}:{}", prefix, key), values);
}
}
debug_event!(
standard = map.standard_fields().len(),
custom = map.custom_fields().len(),
"TagMap extraction complete"
);
map
}
pub(crate) fn tag_map_to_items<T: crate::FileType>(
file: &mut T,
map: &TagMap,
format_name: &str,
) -> Result<ConversionReport> {
use crate::tagmap::mappings;
use crate::tagmap::normalize::{TagSystem, combine_track_disc};
let system = tag_system_for_format(format_name).or_else(|| {
let items = file.items();
detect_tag_system_from_keys(&items).or(Some(TagSystem::APEv2))
});
let mut report = ConversionReport::default();
if map.is_empty() {
return Ok(report);
}
if file.tags().is_none() {
let _ = file.add_tags();
}
debug_event!(format = %format_name, "applying TagMap to format");
let lookup =
|field: &StandardField| -> Option<&'static str> {
match system {
Some(TagSystem::ID3v2) => mappings::standard_to_id3(field)
.or_else(|| mappings::standard_to_id3_txxx(field)),
Some(TagSystem::VorbisComment) => mappings::standard_to_vorbis(field),
Some(TagSystem::MP4) => mappings::standard_to_mp4(field)
.or_else(|| mappings::standard_to_mp4_freeform(field)),
Some(TagSystem::APEv2) => mappings::standard_to_ape(field),
Some(TagSystem::ASF) => mappings::standard_to_asf(field)
.or_else(|| mappings::standard_to_asf_alias(field)),
None => None,
}
};
let track_num = map.get(&StandardField::TrackNumber).map(|v| v.to_vec());
let total_tracks = map.get(&StandardField::TotalTracks).map(|v| v.to_vec());
let disc_num = map.get(&StandardField::DiscNumber).map(|v| v.to_vec());
let total_discs = map.get(&StandardField::TotalDiscs).map(|v| v.to_vec());
for (field, values) in map.standard_fields() {
if matches!(
field,
StandardField::TrackNumber
| StandardField::TotalTracks
| StandardField::DiscNumber
| StandardField::TotalDiscs
) {
continue;
}
if let Some(key) = lookup(field) {
file.set(key, values.to_vec())?;
report.transferred.push(field.clone());
} else {
report
.skipped
.push((field.to_string(), SkipReason::UnsupportedByTarget));
}
}
let needs_combined = matches!(
system,
Some(TagSystem::ID3v2) | Some(TagSystem::APEv2) | Some(TagSystem::MP4)
);
if track_num.is_some() || total_tracks.is_some() {
if needs_combined {
if let Some(key) = lookup(&StandardField::TrackNumber) {
let combined = combine_track_disc(
track_num
.as_ref()
.and_then(|v| v.first())
.map(|s| s.as_str()),
total_tracks
.as_ref()
.and_then(|v| v.first())
.map(|s| s.as_str()),
);
file.set(key, vec![combined])?;
if track_num.is_some() {
report.transferred.push(StandardField::TrackNumber);
}
if total_tracks.is_some() {
report.transferred.push(StandardField::TotalTracks);
}
}
} else {
if let Some(ref vals) = track_num {
if let Some(key) = lookup(&StandardField::TrackNumber) {
file.set(key, vals.clone())?;
report.transferred.push(StandardField::TrackNumber);
}
}
if let Some(ref vals) = total_tracks {
if let Some(key) = lookup(&StandardField::TotalTracks) {
file.set(key, vals.clone())?;
report.transferred.push(StandardField::TotalTracks);
}
}
}
}
if disc_num.is_some() || total_discs.is_some() {
if needs_combined {
if let Some(key) = lookup(&StandardField::DiscNumber) {
let combined = combine_track_disc(
disc_num
.as_ref()
.and_then(|v| v.first())
.map(|s| s.as_str()),
total_discs
.as_ref()
.and_then(|v| v.first())
.map(|s| s.as_str()),
);
file.set(key, vec![combined])?;
if disc_num.is_some() {
report.transferred.push(StandardField::DiscNumber);
}
if total_discs.is_some() {
report.transferred.push(StandardField::TotalDiscs);
}
}
} else {
if let Some(ref vals) = disc_num {
if let Some(key) = lookup(&StandardField::DiscNumber) {
file.set(key, vals.clone())?;
report.transferred.push(StandardField::DiscNumber);
}
}
if let Some(ref vals) = total_discs {
if let Some(key) = lookup(&StandardField::TotalDiscs) {
file.set(key, vals.clone())?;
report.transferred.push(StandardField::TotalDiscs);
}
}
}
}
for (custom_key, values) in map.custom_fields() {
let bare_key = normalize_custom_key_for_transfer(custom_key);
let bare_lower = bare_key.to_lowercase();
if bare_lower.starts_with("apic")
|| bare_lower.contains("picture")
|| bare_lower.contains("cover art")
|| bare_lower.starts_with("rva2")
{
continue;
}
let dest_key = match system {
Some(TagSystem::ID3v2) => format!("TXXX:{}", bare_key),
Some(TagSystem::MP4) => format!("----:com.apple.itunes:{}", bare_key),
_ => bare_key,
};
if let Err(_e) = file.set(&dest_key, values.to_vec()) {
warn_event!(key = %dest_key, error = %_e, "failed to set custom tag");
continue;
}
report.custom_transferred.push(custom_key.to_string());
}
info_event!(
transferred = report.transferred.len(),
custom = report.custom_transferred.len(),
skipped = report.skipped.len(),
"TagMap applied to format"
);
Ok(report)
}
fn normalize_custom_key_for_transfer(key: &str) -> String {
let stripped = key
.strip_prefix("id3:")
.or_else(|| key.strip_prefix("vorbis:"))
.or_else(|| key.strip_prefix("mp4:"))
.or_else(|| key.strip_prefix("ape:"))
.or_else(|| key.strip_prefix("asf:"))
.or_else(|| key.strip_prefix("unknown:"))
.unwrap_or(key);
if let Some(desc) = stripped.strip_prefix("TXXX:") {
return desc.to_string();
}
if let Some(rest) = stripped.strip_prefix("----:") {
if let Some(pos) = rest.find(':') {
return rest[pos + 1..].to_string();
}
return rest.to_string();
}
stripped.to_string()
}
impl DynamicFileType {
pub fn new<T: FileType + 'static>(file: T, filename: Option<PathBuf>) -> Self {
let vtable = vtable_for::<T>();
Self {
inner: Box::new(file),
vtable,
filename,
}
}
pub fn save(&mut self) -> Result<()> {
info_event!(format = %self.format_name(), "saving audio file");
let result = (self.vtable.save)(self.inner.as_mut());
if let Err(_e) = &result {
warn_event!(error = %_e, "failed to save audio file");
} else {
info_event!("file saved successfully");
}
result
}
pub fn save_to_writer(&mut self, writer: &mut dyn ReadWriteSeek) -> Result<()> {
info_event!(format = %self.format_name(), "saving audio file to writer");
let result = (self.vtable.save_to_writer)(self.inner.as_mut(), writer);
if let Err(_e) = &result {
warn_event!(error = %_e, "failed to save audio file to writer");
} else {
info_event!("file saved to writer successfully");
}
result
}
pub fn clear_writer(&mut self, writer: &mut dyn ReadWriteSeek) -> Result<()> {
debug_event!(format = %self.format_name(), "clearing metadata via writer");
let result = (self.vtable.clear_writer)(self.inner.as_mut(), writer);
if let Err(_e) = &result {
warn_event!(error = %_e, "failed to clear metadata via writer");
} else {
debug_event!("metadata cleared via writer successfully");
}
result
}
pub fn save_to_path(&mut self, path: &Path) -> Result<()> {
info_event!(format = %self.format_name(), path = %path.display(), "saving audio file to path");
let result = (self.vtable.save_to_path)(self.inner.as_mut(), path);
if let Err(_e) = &result {
warn_event!(error = %_e, "failed to save audio file to path");
} else {
info_event!("file saved to path successfully");
}
result
}
#[cfg(feature = "async")]
pub async fn save_async(&mut self) -> Result<()> {
let format = self.vtable.format_name;
match format {
"MP3" => {
let file = self
.inner
.downcast_mut::<crate::mp3::MP3>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MP3".to_string())
})?;
file.save_async().await
}
"FLAC" => {
let file = self
.inner
.downcast_mut::<crate::flac::FLAC>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for FLAC".to_string())
})?;
file.save_async().await
}
"MP4" => {
let file = self
.inner
.downcast_mut::<crate::mp4::MP4>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MP4".to_string())
})?;
file.save_async().await
}
"ASF" => {
let file = self
.inner
.downcast_mut::<crate::asf::ASF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for ASF".to_string())
})?;
file.save_async().await
}
"OggVorbis" => {
let file = self
.inner
.downcast_mut::<crate::oggvorbis::OggVorbis>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggVorbis".to_string())
})?;
file.save_async().await
}
"OggOpus" => {
let file = self
.inner
.downcast_mut::<crate::oggopus::OggOpus>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggOpus".to_string())
})?;
file.save_async().await
}
"OggFlac" => {
let file = self
.inner
.downcast_mut::<crate::oggflac::OggFlac>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggFlac".to_string())
})?;
file.save_async().await
}
"OggSpeex" => {
let file = self
.inner
.downcast_mut::<crate::oggspeex::OggSpeex>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggSpeex".to_string())
})?;
file.save_async().await
}
"OggTheora" => {
let file = self
.inner
.downcast_mut::<crate::oggtheora::OggTheora>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggTheora".to_string())
})?;
file.save_async().await
}
"AIFF" => {
let file = self
.inner
.downcast_mut::<crate::aiff::AIFF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for AIFF".to_string())
})?;
file.save_async().await
}
"WAVE" => {
let file = self
.inner
.downcast_mut::<crate::wave::WAVE>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for WAVE".to_string())
})?;
file.save_async().await
}
"DSF" => {
let file = self
.inner
.downcast_mut::<crate::dsf::DSF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for DSF".to_string())
})?;
file.save_async().await
}
"DSDIFF" => {
let file = self
.inner
.downcast_mut::<crate::dsdiff::DSDIFF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for DSDIFF".to_string())
})?;
file.save_async().await
}
"MonkeysAudio" => {
let file = self
.inner
.downcast_mut::<crate::monkeysaudio::MonkeysAudio>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MonkeysAudio".to_string())
})?;
file.save_async().await
}
"WavPack" => {
let file = self
.inner
.downcast_mut::<crate::wavpack::WavPack>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for WavPack".to_string())
})?;
file.save_async().await
}
"TAK" => {
let file = self
.inner
.downcast_mut::<crate::tak::TAK>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for TAK".to_string())
})?;
file.save_async().await
}
"TrueAudio" => {
let file = self
.inner
.downcast_mut::<crate::trueaudio::TrueAudio>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for TrueAudio".to_string())
})?;
file.save_async().await
}
"OptimFROG" => {
let file = self
.inner
.downcast_mut::<crate::optimfrog::OptimFROG>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OptimFROG".to_string())
})?;
file.save_async().await
}
"Musepack" => {
let file = self
.inner
.downcast_mut::<crate::musepack::Musepack>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for Musepack".to_string())
})?;
file.save_async().await
}
"APEv2" => {
let file = self
.inner
.downcast_mut::<crate::apev2::APEv2>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for APEv2".to_string())
})?;
file.save_async().await
}
"ID3" => {
let file = self
.inner
.downcast_mut::<crate::id3::ID3>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for ID3".to_string())
})?;
file.save_async().await
}
"AAC" | "AC3" | "SMF" => Err(AudexError::Unsupported(format!(
"Format {} is read-only and does not support saving",
format
))),
_ => Err(AudexError::Unsupported(format!(
"Async save not supported for format: {}",
format
))),
}
}
pub fn clear(&mut self) -> Result<()> {
debug_event!(format = %self.format_name(), "clearing all metadata");
let result = (self.vtable.clear)(self.inner.as_mut());
if let Err(_e) = &result {
warn_event!(error = %_e, "failed to clear metadata");
} else {
debug_event!("metadata cleared successfully");
}
result
}
pub fn add_tags(&mut self) -> Result<()> {
(self.vtable.add_tags)(self.inner.as_mut())
}
#[cfg(feature = "async")]
pub async fn clear_async(&mut self) -> Result<()> {
let format = self.vtable.format_name;
match format {
"MP3" => {
let file = self
.inner
.downcast_mut::<crate::mp3::MP3>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MP3".to_string())
})?;
file.clear_async().await
}
"FLAC" => {
let file = self
.inner
.downcast_mut::<crate::flac::FLAC>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for FLAC".to_string())
})?;
file.clear_async().await
}
"MP4" => {
let file = self
.inner
.downcast_mut::<crate::mp4::MP4>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MP4".to_string())
})?;
file.clear_async().await
}
"ASF" => {
let file = self
.inner
.downcast_mut::<crate::asf::ASF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for ASF".to_string())
})?;
file.clear_async().await
}
"OggVorbis" => {
let file = self
.inner
.downcast_mut::<crate::oggvorbis::OggVorbis>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggVorbis".to_string())
})?;
file.clear_async().await
}
"OggOpus" => {
let file = self
.inner
.downcast_mut::<crate::oggopus::OggOpus>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggOpus".to_string())
})?;
file.clear_async().await
}
"OggFlac" => {
let file = self
.inner
.downcast_mut::<crate::oggflac::OggFlac>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggFlac".to_string())
})?;
file.clear_async().await
}
"OggSpeex" => {
let file = self
.inner
.downcast_mut::<crate::oggspeex::OggSpeex>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggSpeex".to_string())
})?;
file.clear_async().await
}
"OggTheora" => {
let file = self
.inner
.downcast_mut::<crate::oggtheora::OggTheora>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OggTheora".to_string())
})?;
file.clear_async().await
}
"AIFF" => {
let file = self
.inner
.downcast_mut::<crate::aiff::AIFF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for AIFF".to_string())
})?;
file.clear_async().await
}
"WAVE" => {
let file = self
.inner
.downcast_mut::<crate::wave::WAVE>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for WAVE".to_string())
})?;
file.clear_async().await
}
"DSF" => {
let file = self
.inner
.downcast_mut::<crate::dsf::DSF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for DSF".to_string())
})?;
file.clear_async().await
}
"DSDIFF" => {
let file = self
.inner
.downcast_mut::<crate::dsdiff::DSDIFF>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for DSDIFF".to_string())
})?;
file.clear_async().await
}
"MonkeysAudio" => {
let file = self
.inner
.downcast_mut::<crate::monkeysaudio::MonkeysAudio>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for MonkeysAudio".to_string())
})?;
file.clear_async().await
}
"WavPack" => {
let file = self
.inner
.downcast_mut::<crate::wavpack::WavPack>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for WavPack".to_string())
})?;
file.clear_async().await
}
"TAK" => {
let file = self
.inner
.downcast_mut::<crate::tak::TAK>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for TAK".to_string())
})?;
file.clear_async().await
}
"TrueAudio" => {
let file = self
.inner
.downcast_mut::<crate::trueaudio::TrueAudio>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for TrueAudio".to_string())
})?;
file.clear_async().await
}
"OptimFROG" => {
let file = self
.inner
.downcast_mut::<crate::optimfrog::OptimFROG>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for OptimFROG".to_string())
})?;
file.clear_async().await
}
"Musepack" => {
let file = self
.inner
.downcast_mut::<crate::musepack::Musepack>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for Musepack".to_string())
})?;
file.clear_async().await
}
"APEv2" => {
let file = self
.inner
.downcast_mut::<crate::apev2::APEv2>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for APEv2".to_string())
})?;
file.clear_async().await
}
"ID3" => {
let file = self
.inner
.downcast_mut::<crate::id3::ID3>()
.ok_or_else(|| {
AudexError::InvalidOperation("Type mismatch for ID3".to_string())
})?;
file.clear_async().await
}
"AAC" | "AC3" | "SMF" => Err(AudexError::Unsupported(format!(
"Format {} is read-only and does not support clearing tags",
format
))),
_ => Err(AudexError::Unsupported(format!(
"Async clear not supported for format: {}",
format
))),
}
}
pub fn has_tags(&self) -> bool {
(self.vtable.has_tags)(self.inner.as_ref())
}
pub fn tags_pprint(&self) -> Option<String> {
(self.vtable.tags_pprint)(self.inner.as_ref())
}
pub fn info_pprint(&self) -> String {
(self.vtable.info_pprint)(self.inner.as_ref())
}
pub fn info(&self) -> DynamicStreamInfo {
(self.vtable.info)(self.inner.as_ref())
}
pub fn mime_types(&self) -> &'static [&'static str] {
(self.vtable.mime_types)()
}
pub fn format_name(&self) -> &'static str {
self.vtable.format_name
}
pub fn filename(&self) -> Option<&Path> {
self.filename.as_deref()
}
pub fn downcast_ref<T: FileType + 'static>(&self) -> Option<&T> {
self.inner.downcast_ref::<T>()
}
pub fn downcast_mut<T: FileType + 'static>(&mut self) -> Option<&mut T> {
self.inner.downcast_mut::<T>()
}
#[cfg(feature = "serde")]
pub fn to_snapshot(&self) -> crate::snapshot::TagSnapshot {
trace_event!("building tag snapshot for {}", self.format_name());
let tag_map: std::collections::HashMap<String, Vec<String>> =
self.items().into_iter().collect();
crate::snapshot::TagSnapshot {
format: self.format_name().to_string(),
filename: self.filename().map(|p| p.to_string_lossy().to_string()),
stream_info: crate::snapshot::StreamInfoSnapshot::from_dynamic(&self.info()),
tags: tag_map,
raw_tags: None,
}
}
#[cfg(feature = "serde")]
pub fn to_snapshot_with_raw(&self) -> crate::snapshot::TagSnapshot {
trace_event!(
"building tag snapshot (with raw) for {}",
self.format_name()
);
let mut snapshot = self.to_snapshot();
snapshot.raw_tags = self.try_serialize_raw_tags();
snapshot
}
#[cfg(feature = "serde")]
fn try_serialize_raw_tags(&self) -> Option<serde_json::Value> {
use crate::flac::FLAC;
use crate::mp3::MP3;
use crate::mp4::MP4;
if let Some(mp3) = self.inner.downcast_ref::<MP3>() {
if let Some(ref tags) = mp3.tags {
return serde_json::to_value(tags).ok();
}
}
if let Some(flac) = self.inner.downcast_ref::<FLAC>() {
if let Some(ref tags) = flac.tags {
return serde_json::to_value(tags).ok();
}
}
if let Some(mp4) = self.inner.downcast_ref::<MP4>() {
if let Some(ref tags) = mp4.tags {
return serde_json::to_value(tags).ok();
}
}
None
}
pub fn get(&self, key: &str) -> Option<Vec<String>> {
(self.vtable.get)(self.inner.as_ref(), key)
}
pub fn set(&mut self, key: &str, values: Vec<String>) -> Result<()> {
(self.vtable.set)(self.inner.as_mut(), key, values)
}
pub fn remove(&mut self, key: &str) -> Result<()> {
(self.vtable.remove)(self.inner.as_mut(), key)
}
pub fn keys(&self) -> Vec<String> {
(self.vtable.keys)(self.inner.as_ref())
}
pub fn contains_key(&self, key: &str) -> bool {
(self.vtable.contains_key)(self.inner.as_ref(), key)
}
pub fn get_first(&self, key: &str) -> Option<String> {
(self.vtable.get_first)(self.inner.as_ref(), key)
}
pub fn set_single(&mut self, key: &str, value: String) -> Result<()> {
(self.vtable.set_single)(self.inner.as_mut(), key, value)
}
pub fn len(&self) -> usize {
(self.vtable.len)(self.inner.as_ref())
}
pub fn is_empty(&self) -> bool {
(self.vtable.is_empty)(self.inner.as_ref())
}
pub fn items(&self) -> Vec<(String, Vec<String>)> {
(self.vtable.items)(self.inner.as_ref())
}
pub fn values(&self) -> Vec<Vec<String>> {
self.items().into_iter().map(|(_, v)| v).collect()
}
pub fn update(&mut self, other: Vec<(String, Vec<String>)>) -> Result<()> {
for (key, values) in other {
self.set(&key, values)?;
}
Ok(())
}
pub fn get_or(&self, key: &str, default: Vec<String>) -> Vec<String> {
(self.vtable.get_or)(self.inner.as_ref(), key, default)
}
pub fn pop(&mut self, key: &str) -> Result<Option<Vec<String>>> {
(self.vtable.pop)(self.inner.as_mut(), key)
}
pub fn pop_or(&mut self, key: &str, default: Vec<String>) -> Result<Vec<String>> {
(self.vtable.pop_or)(self.inner.as_mut(), key, default)
}
pub fn diff_tags(&self, other: &DynamicFileType) -> crate::diff::TagDiff {
crate::diff::diff(self, other)
}
pub fn diff_tags_with_options(
&self,
other: &DynamicFileType,
options: &crate::diff::DiffOptions,
) -> crate::diff::TagDiff {
crate::diff::diff_with_options(self, other, options)
}
pub fn tag_snapshot_items(&self) -> Vec<(String, Vec<String>)> {
self.items()
}
pub fn diff_against(&self, snapshot: &[(String, Vec<String>)]) -> crate::diff::TagDiff {
crate::diff::diff_against_snapshot(self, snapshot)
}
pub fn diff_tags_normalized(&self, other: &DynamicFileType) -> crate::diff::TagDiff {
crate::diff::diff_normalized(self, other)
}
pub fn diff_tags_normalized_with_options(
&self,
other: &DynamicFileType,
options: &crate::diff::DiffOptions,
) -> crate::diff::TagDiff {
crate::diff::diff_normalized_with_options(self, other, options)
}
pub fn to_tag_map(&self) -> TagMap {
(self.vtable.to_tag_map)(self.inner.as_ref())
}
pub fn apply_tag_map(&mut self, map: &TagMap) -> Result<ConversionReport> {
(self.vtable.apply_tag_map)(self.inner.as_mut(), map)
}
pub fn import_tags_from(&mut self, source: &DynamicFileType) -> Result<ConversionReport> {
crate::tagmap::convert_tags(source, self)
}
pub fn import_tags_from_with_options(
&mut self,
source: &DynamicFileType,
options: &crate::tagmap::ConversionOptions,
) -> Result<ConversionReport> {
crate::tagmap::convert_tags_with_options(source, self, options)
}
}
impl fmt::Debug for DynamicFileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DynamicFileType")
.field("format", &self.vtable.format_name)
.field("filename", &self.filename)
.field("has_tags", &self.has_tags())
.finish()
}
}
impl fmt::Display for DynamicFileType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<{}", self.format_name())?;
if let Some(filename) = self.filename() {
write!(f, " '{}'>", filename.display())?;
} else {
write!(f, " '(no filename)'>")?
}
Ok(())
}
}
pub struct DynamicFileTypeIter {
items: std::vec::IntoIter<(String, Vec<String>)>,
}
impl Iterator for DynamicFileTypeIter {
type Item = (String, Vec<String>);
fn next(&mut self) -> Option<Self::Item> {
self.items.next()
}
}
impl IntoIterator for &DynamicFileType {
type Item = (String, Vec<String>);
type IntoIter = DynamicFileTypeIter;
fn into_iter(self) -> Self::IntoIter {
DynamicFileTypeIter {
items: self.items().into_iter(),
}
}
}
fn load_file_from_path(path: &Path) -> Result<DynamicFileType> {
debug_event!(path = %path.display(), "opening file for format detection");
let mut file = StdFile::open(path)?;
let mut header = [0u8; 128];
let bytes_read = file.read(&mut header)?;
let descriptor = score_all_formats(path, &header[..bytes_read])?;
debug_event!(format = %descriptor.name, "format detected, loading file");
load_with_descriptor(descriptor, path, None)
}
fn score_all_formats(path: &Path, header: &[u8]) -> Result<&'static FormatDescriptor> {
let filename = path.to_string_lossy();
let mut best_score = 0;
let mut best_descriptor: Option<&FormatDescriptor> = None;
for descriptor in FORMAT_REGISTRY {
let score = descriptor.score(&filename, header);
trace_event!(format = %descriptor.name, score = score, "format score");
if score > best_score {
best_score = score;
best_descriptor = Some(descriptor);
}
}
match (best_descriptor, best_score) {
(Some(descriptor), score) if score > 0 => {
debug_event!(format = %descriptor.name, score = score, "winning format detected");
Ok(descriptor)
}
_ => {
warn_event!(path = %path.display(), "no format could handle this file");
Err(AudexError::UnsupportedFormat(
"No format could handle this file".to_string(),
))
}
}
}
fn load_with_descriptor(
descriptor: &FormatDescriptor,
path: &Path,
_file_thing: Option<AnyFileThing>,
) -> Result<DynamicFileType> {
descriptor.load(path)
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, fields(path = %path.as_ref().display())))]
pub fn detect_format<P: AsRef<Path>>(path: P) -> Result<String> {
let path = path.as_ref();
debug_event!("detecting audio format");
let mut file = StdFile::open(path)?;
let mut header = [0u8; 128];
let bytes_read = file.read(&mut header)?;
let descriptor = score_all_formats(path, &header[..bytes_read])?;
debug_event!(format = %descriptor.name, "format detection complete");
Ok(descriptor.name.to_string())
}
pub fn detect_format_from_bytes(data: &[u8], filename_hint: Option<&Path>) -> Result<String> {
let fallback = PathBuf::from("unknown");
let path = filename_hint.unwrap_or(&fallback);
let header_len = data.len().min(128);
let descriptor = score_all_formats(path, &data[..header_len])?;
Ok(descriptor.name.to_string())
}
pub struct FileStruct;
impl FileStruct {
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, fields(path = %path.as_ref().display())))]
pub fn load<P: AsRef<Path>>(path: P) -> Result<DynamicFileType> {
info_event!("loading audio file");
let result = load_file_from_path(path.as_ref());
if let Ok(_file) = &result {
let _format_name = _file.format_name();
info_event!(format = %_format_name, "file loaded successfully");
} else if let Err(_e) = &result {
warn_event!(error = %_e, "failed to load audio file");
}
result
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip(reader), fields(origin_path = ?origin_path)))]
pub fn load_from_reader<R: Read + Seek>(
mut reader: R,
origin_path: Option<PathBuf>,
) -> Result<DynamicFileType> {
info_event!("loading audio file from reader");
let mut header = [0u8; 128];
let bytes_read = reader.read(&mut header)?;
reader.seek(std::io::SeekFrom::Start(0))?;
let fallback_path = PathBuf::from("unknown");
let detect_path = origin_path.as_ref().unwrap_or(&fallback_path);
let descriptor = score_all_formats(detect_path, &header[..bytes_read])?;
debug_event!(format = %descriptor.name, "format detected from reader");
if let Some(reader_loader) = descriptor.load_from_reader_fn {
let result = reader_loader(&mut reader);
if let Err(_e) = &result {
warn_event!(error = %_e, format = %descriptor.name, "failed to load from reader");
} else {
info_event!(format = %descriptor.name, "file loaded successfully from reader");
}
return result;
}
let err = AudexError::Unsupported(format!(
"Format '{}' does not support loading from a reader",
descriptor.name
));
warn_event!(error = %err, "reader-based loading not supported for format");
Err(err)
}
#[cfg(feature = "async")]
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<DynamicFileType> {
use tokio::io::AsyncReadExt;
let path = path.as_ref();
let mut file = TokioFile::open(path).await?;
let mut header = [0u8; 128];
let bytes_read = file.read(&mut header).await?;
let filename = path.to_string_lossy();
let mut best_score = 0;
let mut best_format = "";
for descriptor in FORMAT_REGISTRY {
let score = descriptor.score(&filename, &header[..bytes_read]);
if score > best_score {
best_score = score;
best_format = descriptor.name;
}
}
if best_score == 0 {
return Err(AudexError::UnsupportedFormat(
"No format could handle this file".to_string(),
));
}
let path_buf = Some(path.to_path_buf());
match best_format {
"MP3" => {
let f = crate::mp3::MP3::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"FLAC" => {
let f = crate::flac::FLAC::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"MP4" => {
let f = crate::mp4::MP4::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"ASF" => {
let f = crate::asf::ASF::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OggVorbis" => {
let f = crate::oggvorbis::OggVorbis::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OggOpus" => {
let f = crate::oggopus::OggOpus::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OggFlac" => {
let f = crate::oggflac::OggFlac::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OggSpeex" => {
let f = crate::oggspeex::OggSpeex::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OggTheora" => {
let f = crate::oggtheora::OggTheora::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"AIFF" => {
let f = crate::aiff::AIFF::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"WAVE" => {
let f = crate::wave::WAVE::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"DSF" => {
let f = crate::dsf::DSF::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"DSDIFF" => {
let f = crate::dsdiff::DSDIFF::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"MonkeysAudio" => {
let f = crate::monkeysaudio::MonkeysAudio::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"WavPack" => {
let f = crate::wavpack::WavPack::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"TAK" => {
let f = crate::tak::TAK::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"TrueAudio" => {
let f = crate::trueaudio::TrueAudio::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"OptimFROG" => {
let f = crate::optimfrog::OptimFROG::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"Musepack" => {
let f = crate::musepack::Musepack::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"APEv2" => {
let f = crate::apev2::APEv2::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"AAC" => {
let f = crate::aac::AAC::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
"SMF" => {
let f = crate::smf::SMF::load_async(path).await?;
Ok(DynamicFileType::new(f, path_buf))
}
_ => Err(AudexError::Unsupported(format!(
"Async loading not supported for format: {}",
best_format
))),
}
}
#[cfg(feature = "async")]
pub async fn load_from_buffer_async(
data: Vec<u8>,
origin_path: Option<PathBuf>,
) -> Result<DynamicFileType> {
let header_len = data.len().min(128);
let fallback_path = PathBuf::from("unknown");
let detect_path = origin_path.as_ref().unwrap_or(&fallback_path);
let descriptor = score_all_formats(detect_path, &data[..header_len])?;
if let Some(reader_loader) = descriptor.load_from_reader_fn {
let mut cursor = std::io::Cursor::new(data);
return reader_loader(&mut cursor);
}
Err(AudexError::Unsupported(format!(
"Format '{}' does not support loading from a reader",
descriptor.name
)))
}
}
pub use FileStruct as File;
#[cfg(feature = "async")]
pub async fn detect_format_async<P: AsRef<Path>>(path: P) -> Result<String> {
use tokio::io::AsyncReadExt;
let path = path.as_ref();
let mut file = TokioFile::open(path).await?;
let mut header = [0u8; 128];
let bytes_read = file.read(&mut header).await?;
detect_format_from_header(path, &header[..bytes_read])
}
#[cfg(feature = "async")]
fn detect_format_from_header(path: &Path, header: &[u8]) -> Result<String> {
let filename = path.to_string_lossy();
let format = if header.len() >= 4 {
if header.starts_with(b"ID3") {
"MP3"
} else if header.starts_with(b"fLaC") {
"FLAC"
} else if header.len() >= 8 && &header[4..8] == b"ftyp" {
"MP4"
} else if header.starts_with(b"OggS") {
detect_ogg_format(header)
} else if header.starts_with(b"RIFF") {
"WAVE"
} else if header.starts_with(b"FORM") {
"AIFF"
} else if header.starts_with(&[0x30, 0x26, 0xB2, 0x75]) {
"ASF"
} else if header.starts_with(b"DSD ") {
"DSF"
} else if header.starts_with(b"FRM8") {
"DSDIFF"
} else if header.starts_with(b"MAC ") {
"MonkeysAudio"
} else if header.starts_with(b"wvpk") {
"WavPack"
} else if header.starts_with(b"MThd") {
"SMF"
} else if is_mp3_frame_for_async(header) {
"MP3"
} else {
detect_by_extension_for_async(&filename)
}
} else {
detect_by_extension_for_async(&filename)
};
if format == "Unknown" {
Err(AudexError::UnsupportedFormat(
"No format could handle this file".to_string(),
))
} else {
Ok(format.to_string())
}
}
pub fn detect_ogg_format(header: &[u8]) -> &'static str {
if header.len() >= 36 {
if header[28..].starts_with(b"OpusHead") {
return "OggOpus";
}
if header[29..].starts_with(b"vorbis") {
return "OggVorbis";
}
if header[29..].starts_with(b"FLAC") {
return "OggFlac";
}
if header[28..].starts_with(b"Speex") {
return "OggSpeex";
}
return "Ogg";
}
"Ogg"
}
#[cfg(feature = "async")]
fn detect_by_extension_for_async(filename: &str) -> &'static str {
let lower = filename.to_lowercase();
if lower.ends_with(".mp3") || lower.ends_with(".mp2") {
"MP3"
} else if lower.ends_with(".flac") {
"FLAC"
} else if lower.ends_with(".m4a")
|| lower.ends_with(".mp4")
|| lower.ends_with(".m4b")
|| lower.ends_with(".m4p")
{
"MP4"
} else if lower.ends_with(".ogg") {
"OggVorbis"
} else if lower.ends_with(".opus") {
"OggOpus"
} else if lower.ends_with(".wav") {
"WAVE"
} else if lower.ends_with(".aiff") || lower.ends_with(".aif") {
"AIFF"
} else if lower.ends_with(".wma") || lower.ends_with(".asf") {
"ASF"
} else if lower.ends_with(".dsf") {
"DSF"
} else if lower.ends_with(".dff") {
"DSDIFF"
} else if lower.ends_with(".ape") {
"MonkeysAudio"
} else if lower.ends_with(".wv") {
"WavPack"
} else if lower.ends_with(".mid") || lower.ends_with(".midi") {
"SMF"
} else if lower.ends_with(".aac") {
"AAC"
} else if lower.ends_with(".ac3") {
"AC3"
} else if lower.ends_with(".mpc") {
"Musepack"
} else if lower.ends_with(".tak") {
"TAK"
} else if lower.ends_with(".tta") {
"TrueAudio"
} else if lower.ends_with(".ofr") || lower.ends_with(".ofs") {
"OptimFROG"
} else if lower.ends_with(".spx") {
"OggSpeex"
} else if lower.ends_with(".ogv") {
"OggTheora"
} else {
"Unknown"
}
}
#[cfg(feature = "async")]
fn is_mp3_frame_for_async(header: &[u8]) -> bool {
if header.len() < 2 {
return false;
}
header[0] == 0xFF && (header[1] & 0xE0) == 0xE0
}