use crate::mp4::{AtomDataType, EasyMP4KeyError, MP4, MP4FreeForm, MP4Info, MP4Tags};
use crate::tags::{Metadata, MetadataFields, Tags};
use crate::{AudexError, FileType, Result};
use std::collections::HashMap;
use std::path::Path;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum KeyType {
Text,
Integer,
IntegerPair,
Freeform,
}
#[derive(Debug, Clone)]
pub struct KeyMapping {
pub mp4_key: String,
pub easy_key: String,
pub key_type: KeyType,
}
#[derive(Debug, Default)]
pub struct KeyRegistry {
easy_to_mp4: HashMap<String, KeyMapping>,
mp4_to_easy: HashMap<String, KeyMapping>,
}
impl KeyRegistry {
pub fn new() -> Self {
let mut registry = Self::default();
registry.register_default_keys();
registry
}
pub fn register_text_key(&mut self, mp4_key: &str, easy_key: &str) {
debug_event!(key = %easy_key, mp4_key = %mp4_key, "registered EasyMP4 text key");
let mapping = KeyMapping {
mp4_key: mp4_key.to_string(),
easy_key: easy_key.to_string(),
key_type: KeyType::Text,
};
self.easy_to_mp4
.insert(easy_key.to_lowercase(), mapping.clone());
self.mp4_to_easy.insert(mp4_key.to_string(), mapping);
}
pub fn register_int_key(&mut self, mp4_key: &str, easy_key: &str) {
let mapping = KeyMapping {
mp4_key: mp4_key.to_string(),
easy_key: easy_key.to_string(),
key_type: KeyType::Integer,
};
self.easy_to_mp4
.insert(easy_key.to_lowercase(), mapping.clone());
self.mp4_to_easy.insert(mp4_key.to_string(), mapping);
}
pub fn register_int_pair_key(&mut self, mp4_key: &str, easy_key: &str) {
let mapping = KeyMapping {
mp4_key: mp4_key.to_string(),
easy_key: easy_key.to_string(),
key_type: KeyType::IntegerPair,
};
self.easy_to_mp4
.insert(easy_key.to_lowercase(), mapping.clone());
self.mp4_to_easy.insert(mp4_key.to_string(), mapping);
}
pub fn register_freeform_key(&mut self, freeform_key: &str, easy_key: &str) {
debug_event!(key = %easy_key, freeform_key = %freeform_key, "registered EasyMP4 freeform key");
let mp4_key = format!("----:com.apple.itunes:{}", freeform_key);
let mapping = KeyMapping {
mp4_key,
easy_key: easy_key.to_string(),
key_type: KeyType::Freeform,
};
self.easy_to_mp4
.insert(easy_key.to_lowercase(), mapping.clone());
self.mp4_to_easy.insert(mapping.mp4_key.clone(), mapping);
}
pub fn get_mp4_key(&self, easy_key: &str) -> Option<&KeyMapping> {
self.easy_to_mp4.get(&easy_key.to_lowercase())
}
pub fn get_easy_key(&self, mp4_key: &str) -> Option<&KeyMapping> {
self.mp4_to_easy.get(mp4_key)
}
fn register_default_keys(&mut self) {
self.register_text_key("©nam", "title");
self.register_text_key("©alb", "album");
self.register_text_key("©ART", "artist");
self.register_text_key("aART", "albumartist");
self.register_text_key("©day", "date");
self.register_text_key("©cmt", "comment");
self.register_text_key("desc", "description");
self.register_text_key("©grp", "grouping");
self.register_text_key("©gen", "genre");
self.register_text_key("©wrt", "composer");
self.register_text_key("cprt", "copyright");
self.register_text_key("soal", "albumsort");
self.register_text_key("soaa", "albumartistsort");
self.register_text_key("soar", "artistsort");
self.register_text_key("sonm", "titlesort");
self.register_text_key("soco", "composersort");
self.register_int_key("tmpo", "bpm");
self.register_int_key("cpil", "compilation");
self.register_int_pair_key("trkn", "tracknumber");
self.register_int_pair_key("disk", "discnumber");
self.register_text_key("\u{00A9}too", "encodingsoftware");
self.register_freeform_key("MusicBrainz Artist Id", "musicbrainz_artistid");
self.register_freeform_key("MusicBrainz Track Id", "musicbrainz_trackid");
self.register_freeform_key("MusicBrainz Album Id", "musicbrainz_albumid");
self.register_freeform_key("MusicBrainz Album Artist Id", "musicbrainz_albumartistid");
self.register_freeform_key("MusicIP PUID", "musicip_puid");
self.register_freeform_key("MusicBrainz Album Status", "musicbrainz_albumstatus");
self.register_freeform_key("MusicBrainz Album Type", "musicbrainz_albumtype");
self.register_freeform_key("MusicBrainz Release Country", "releasecountry");
}
}
#[derive(Debug)]
pub struct EasyMP4Tags {
pub tags: MP4Tags,
registry: KeyRegistry,
}
impl EasyMP4Tags {
pub fn new(tags: MP4Tags) -> Self {
Self {
tags,
registry: KeyRegistry::new(),
}
}
pub fn empty() -> Self {
Self::new(MP4Tags::new())
}
pub fn get(&self, key: &str) -> Result<Option<Vec<String>>> {
trace_event!(key = %key, "EasyMP4 get");
let mapping = self
.registry
.get_mp4_key(key)
.ok_or_else(|| EasyMP4KeyError::new(key))?;
match mapping.key_type {
KeyType::Text | KeyType::Integer => {
if let Some(values) = self.tags.get(&mapping.mp4_key) {
Ok(Some(values.to_vec()))
} else {
let corrupted_key = mapping.mp4_key.replace('©', "�");
if corrupted_key != mapping.mp4_key {
if let Some(values) = self.tags.get(&corrupted_key) {
Ok(Some(values.to_vec()))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}
KeyType::IntegerPair => {
if let Some(values) = self.tags.get(&mapping.mp4_key) {
Ok(Some(values.to_vec()))
} else {
let corrupted_key = mapping.mp4_key.replace('©', "�");
if corrupted_key != mapping.mp4_key {
if let Some(values) = self.tags.get(&corrupted_key) {
Ok(Some(values.to_vec()))
} else {
Ok(None)
}
} else {
Ok(None)
}
}
}
KeyType::Freeform => {
if let Some(freeforms) = self.tags.freeforms.get(&mapping.mp4_key) {
let values: Vec<String> = freeforms
.iter()
.map(|f| String::from_utf8_lossy(&f.data).into_owned())
.collect();
Ok(Some(values))
} else {
Ok(None)
}
}
}
}
pub fn set(&mut self, key: &str, values: Vec<String>) -> Result<()> {
trace_event!(key = %key, count = values.len(), "EasyMP4 set");
let mapping = self
.registry
.get_mp4_key(key)
.ok_or_else(|| EasyMP4KeyError::new(key))?
.clone();
if values.is_empty() {
return self.remove(key);
}
match mapping.key_type {
KeyType::Text => {
let normalized_key = self.normalize_mp4_key(&mapping.mp4_key);
self.tags.set(&normalized_key, values);
Ok(())
}
KeyType::Integer => {
let int_values: Result<Vec<String>> = values
.into_iter()
.map(|v| {
let i = v.parse::<i32>().map_err(|_| {
AudexError::ParseError(format!("'{}' is not an integer", v))
})?;
if i < 0 {
return Err(AudexError::ParseError(format!(
"'{}' is negative — integer tag values must be non-negative",
v
)));
}
if i > 65535 {
return Err(AudexError::ParseError(format!(
"Value {} exceeds MP4 integer limit of 65535",
i
)));
}
Ok(i.to_string())
})
.collect();
let normalized_key = self.normalize_mp4_key(&mapping.mp4_key);
self.tags.set(&normalized_key, int_values?);
Ok(())
}
KeyType::IntegerPair => {
let pair_values: Result<Vec<String>> = values
.into_iter()
.map(|v| {
if v.contains('/') {
let parts: Vec<&str> = v.split('/').collect();
if parts.len() != 2 {
return Err(AudexError::ParseError(format!(
"'{}' is not a valid integer pair",
v
)));
}
let track: u32 = parts[0].parse().map_err(|_| {
AudexError::ParseError(format!(
"'{}' is not a valid integer pair",
v
))
})?;
let total: u32 = parts[1].parse().map_err(|_| {
AudexError::ParseError(format!(
"'{}' is not a valid integer pair",
v
))
})?;
if track > 65535 || total > 65535 {
return Err(AudexError::ParseError(format!(
"Value in '{}' exceeds MP4 integer limit of 65535",
v
)));
}
Ok(format!("{}/{}", track, total))
} else {
let track: u32 = v.parse().map_err(|_| {
AudexError::ParseError(format!("'{}' is not a valid integer", v))
})?;
if track > 65535 {
return Err(AudexError::ParseError(format!(
"Value {} exceeds MP4 integer limit of 65535",
track
)));
}
Ok(track.to_string())
}
})
.collect();
let normalized_key = self.normalize_mp4_key(&mapping.mp4_key);
self.tags.set(&normalized_key, pair_values?);
Ok(())
}
KeyType::Freeform => {
let freeforms: Vec<MP4FreeForm> = values
.into_iter()
.map(|v| MP4FreeForm::new(v.into_bytes(), AtomDataType::Utf8, 0))
.collect();
self.tags.freeforms.insert(mapping.mp4_key, freeforms);
Ok(())
}
}
}
pub fn remove(&mut self, key: &str) -> Result<()> {
let mapping = self
.registry
.get_mp4_key(key)
.ok_or_else(|| EasyMP4KeyError::new(key))?;
match mapping.key_type {
KeyType::Text | KeyType::Integer | KeyType::IntegerPair => {
self.tags.remove(&mapping.mp4_key);
let corrupted_key = mapping.mp4_key.replace('\u{00A9}', "\u{FFFD}");
if corrupted_key != mapping.mp4_key {
self.tags.remove(&corrupted_key);
}
Ok(())
}
KeyType::Freeform => {
self.tags.freeforms.remove(&mapping.mp4_key);
Ok(())
}
}
}
pub fn keys(&self) -> Vec<String> {
let mut keys = Vec::new();
for mp4_key in self.tags.keys() {
if let Some(mapping) = self.registry.get_easy_key(&mp4_key) {
keys.push(mapping.easy_key.clone());
}
}
for mp4_key in self.tags.freeforms.keys() {
if let Some(mapping) = self.registry.get_easy_key(mp4_key) {
keys.push(mapping.easy_key.clone());
}
}
keys.sort();
keys.dedup();
keys
}
pub fn contains_key(&self, key: &str) -> bool {
if let Ok(values) = self.get(key) {
values.is_some()
} else {
false
}
}
fn normalize_mp4_key(&self, key: &str) -> String {
key.replace('�', "©")
}
pub fn mp4_tags(&self) -> &MP4Tags {
&self.tags
}
pub fn mp4_tags_mut(&mut self) -> &mut MP4Tags {
&mut self.tags
}
pub fn load<R: std::io::Read + std::io::Seek>(
atoms: &crate::mp4::atom::Atoms,
reader: &mut R,
) -> Result<Option<Self>> {
trace_event!("loading EasyMP4Tags from atoms");
if let Some(mp4_tags) = MP4Tags::load(atoms, reader)? {
Ok(Some(Self::new(mp4_tags)))
} else {
Ok(None)
}
}
pub fn can_load(atoms: &crate::mp4::atom::Atoms) -> bool {
MP4Tags::can_load(atoms)
}
}
impl Tags for EasyMP4Tags {
fn get(&self, key: &str) -> Option<&[String]> {
let matched_key = self.resolve_easy_key(key)?;
if let Ok(Some(_values)) = EasyMP4Tags::get(self, &matched_key) {
if let Some(mapping) = self.registry.get_mp4_key(&matched_key) {
match mapping.key_type {
KeyType::Text | KeyType::Integer | KeyType::IntegerPair => {
self.tags.get(&mapping.mp4_key)
}
KeyType::Freeform => None,
}
} else {
None
}
} else {
None
}
}
fn set(&mut self, key: &str, values: Vec<String>) {
if let Some(matched_key) = self.resolve_easy_key(key) {
if let Err(_e) = EasyMP4Tags::set(self, &matched_key, values) {
warn_event!(key = %matched_key, error = %_e, "EasyMP4 trait set failed");
}
} else {
if let Err(_e) = EasyMP4Tags::set(self, key, values) {
warn_event!(key = %key, error = %_e, "EasyMP4 trait set failed");
}
}
}
fn remove(&mut self, key: &str) {
if let Some(matched_key) = self.resolve_easy_key(key) {
if let Err(_e) = EasyMP4Tags::remove(self, &matched_key) {
warn_event!(key = %matched_key, error = %_e, "EasyMP4 trait remove failed");
}
} else if let Err(_e) = EasyMP4Tags::remove(self, key) {
warn_event!(key = %key, error = %_e, "EasyMP4 trait remove failed");
}
}
fn keys(&self) -> Vec<String> {
self.keys()
}
fn pprint(&self) -> String {
let mut result = String::new();
let keys = self.keys();
for key in keys {
if let Ok(Some(values)) = self.get(&key) {
for value in values {
if !result.is_empty() {
result.push('\n');
}
result.push_str(&format!("{}={}", key, value));
}
}
}
result
}
}
impl EasyMP4Tags {
fn resolve_easy_key(&self, key: &str) -> Option<String> {
self.keys()
.into_iter()
.find(|candidate| candidate.eq_ignore_ascii_case(key))
.or_else(|| {
self.registry
.get_mp4_key(key)
.map(|mapping| mapping.easy_key.clone())
})
}
}
impl Metadata for EasyMP4Tags {
type Error = AudexError;
fn new() -> Self {
Self::empty()
}
fn load_from_fileobj(filething: &mut crate::util::AnyFileThing) -> Result<Self> {
let atoms = crate::mp4::atom::Atoms::parse(filething)?;
if let Some(mp4_tags) = MP4Tags::load(&atoms, filething)? {
Ok(Self::new(mp4_tags))
} else {
Ok(Self::empty())
}
}
fn save_to_fileobj(&self, filething: &mut crate::util::AnyFileThing) -> Result<()> {
if let Some(path) = filething.filename() {
self.tags.save(path)
} else {
Err(AudexError::NotImplementedMethod(
"save_to_fileobj requires a file path for MP4 format".to_string(),
))
}
}
fn delete_from_fileobj(filething: &mut crate::util::AnyFileThing) -> Result<()> {
if let Some(path) = filething.filename() {
let empty_tags = MP4Tags::new();
empty_tags.save(path)
} else {
Err(AudexError::NotImplementedMethod(
"delete_from_fileobj requires a file path for MP4 format".to_string(),
))
}
}
}
impl MetadataFields for EasyMP4Tags {
fn artist(&self) -> Option<&String> {
if let Ok(Some(_values)) = self.get("artist") {
self.tags.get_first("©ART")
} else {
None
}
}
fn set_artist(&mut self, artist: String) {
let _ = self.set("artist", vec![artist]);
}
fn album(&self) -> Option<&String> {
self.tags.get_first("©alb")
}
fn set_album(&mut self, album: String) {
let _ = self.set("album", vec![album]);
}
fn title(&self) -> Option<&String> {
self.tags.get_first("©nam")
}
fn set_title(&mut self, title: String) {
let _ = self.set("title", vec![title]);
}
fn track_number(&self) -> Option<u32> {
if let Ok(Some(values)) = self.get("tracknumber") {
values.first()?.split('/').next()?.parse().ok()
} else {
None
}
}
fn set_track_number(&mut self, track: u32) {
let _ = self.set("tracknumber", vec![track.to_string()]);
}
fn date(&self) -> Option<&String> {
self.tags.get_first("©day")
}
fn set_date(&mut self, date: String) {
let _ = self.set("date", vec![date]);
}
fn genre(&self) -> Option<&String> {
self.tags.get_first("©gen")
}
fn set_genre(&mut self, genre: String) {
let _ = self.set("genre", vec![genre]);
}
}
#[derive(Debug)]
pub struct EasyMP4 {
path: Option<std::path::PathBuf>,
pub info: MP4Info,
tags: Option<EasyMP4Tags>,
}
impl EasyMP4 {
pub fn new() -> Self {
Self {
path: None,
info: MP4Info::default(),
tags: None,
}
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::ParseError("tags already exist".to_string()));
}
self.tags = Some(EasyMP4Tags::empty());
Ok(())
}
pub fn get_or_create_tags(&mut self) -> Result<&mut EasyMP4Tags> {
if self.tags.is_none() {
self.tags = Some(EasyMP4Tags::empty());
}
self.tags
.as_mut()
.ok_or_else(|| AudexError::InvalidOperation("No tags available".to_string()))
}
pub fn register_text_key(&mut self, easy_key: &str, mp4_atom_path: &str) -> Result<()> {
let tags = self.get_or_create_tags()?;
if mp4_atom_path.starts_with("----:") {
let mapping = KeyMapping {
mp4_key: mp4_atom_path.to_string(),
easy_key: easy_key.to_string(),
key_type: KeyType::Freeform,
};
tags.registry
.easy_to_mp4
.insert(easy_key.to_lowercase(), mapping.clone());
tags.registry
.mp4_to_easy
.insert(mp4_atom_path.to_string(), mapping);
} else {
tags.registry.register_text_key(mp4_atom_path, easy_key);
}
Ok(())
}
pub fn get(&self, key: &str) -> Option<Vec<String>> {
self.tags.as_ref()?.get(key).ok().flatten()
}
pub fn set(&mut self, key: &str, values: Vec<String>) -> Result<()> {
let tags = self.get_or_create_tags()?;
tags.set(key, values)
}
pub fn remove(&mut self, key: &str) -> Result<()> {
if let Some(ref mut tags) = self.tags {
tags.remove(key)
} else {
Ok(())
}
}
pub fn is_empty(&self) -> bool {
self.tags.as_ref().is_none_or(|tags| tags.keys().is_empty())
}
pub fn keys(&self) -> Vec<String> {
self.tags.as_ref().map_or(Vec::new(), |tags| tags.keys())
}
}
impl Default for EasyMP4 {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "async")]
impl EasyMP4 {
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let mp4 = crate::mp4::MP4::load_async(path).await?;
let tags = mp4.tags.map(EasyMP4Tags::new);
Ok(EasyMP4 {
path: Some(path.to_path_buf()),
info: mp4.info,
tags,
})
}
pub async fn save_async(&mut self) -> Result<()> {
if let Some(path) = &self.path {
if let Some(tags) = &self.tags {
tags.mp4_tags().save_async(path).await?;
}
} else {
return Err(AudexError::ParseError(
"No file path available for saving".to_string(),
));
}
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
self.tags = Some(EasyMP4Tags::empty());
if self.path.is_some() {
self.save_async().await
} else {
Ok(())
}
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
crate::mp4::util::clear_async(path).await
}
}
impl FileType for EasyMP4 {
type Tags = EasyMP4Tags;
type Info = MP4Info;
fn format_id() -> &'static str {
"EasyMP4"
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(format = "EasyMP4"))
)]
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
info_event!(path = %path.as_ref().display(), "loading EasyMP4 tags");
let mp4 = MP4::load(&path)?;
let tags = mp4.tags.map(EasyMP4Tags::new);
Ok(EasyMP4 {
path: Some(path.as_ref().to_path_buf()),
info: mp4.info,
tags,
})
}
fn save(&mut self) -> Result<()> {
if let Some(path) = &self.path {
if let Some(tags) = &self.tags {
tags.mp4_tags().save(path)
} else {
Ok(())
}
} else {
Err(AudexError::ParseError(
"No file path available for saving".to_string(),
))
}
}
fn clear(&mut self) -> Result<()> {
self.tags = Some(EasyMP4Tags::empty());
if self.path.is_some() {
self.save()
} else {
Ok(())
}
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(EasyMP4Tags::empty());
Ok(())
}
fn tags(&self) -> Option<&Self::Tags> {
self.tags.as_ref()
}
fn tags_mut(&mut self) -> Option<&mut Self::Tags> {
self.tags.as_mut()
}
fn info(&self) -> &Self::Info {
&self.info
}
fn score(filename: &str, header: &[u8]) -> i32 {
MP4::score(filename, header).saturating_sub(1)
}
fn mime_types() -> &'static [&'static str] {
&["audio/mp4", "audio/x-m4a"]
}
}
pub fn register_text_key(_mp4_key: &str, _easy_key: &str) {
warn_event!("register_text_key not implemented - keys are hardcoded");
}
pub fn register_int_key(_mp4_key: &str, _easy_key: &str) {
warn_event!("register_int_key not implemented - keys are hardcoded");
}
pub fn register_int_pair_key(_mp4_key: &str, _easy_key: &str) {
warn_event!("register_int_pair_key not implemented - keys are hardcoded");
}
pub fn register_freeform_key(_freeform_key: &str, _easy_key: &str) {
warn_event!("register_freeform_key not implemented - keys are hardcoded");
}