use crate::id3::ID3Tags;
use crate::{AudexError, FileType, Result};
use globset::{Glob, GlobMatcher};
use std::collections::HashMap;
use std::path::Path;
use std::sync::LazyLock;
use std::sync::Mutex;
macro_rules! lock_or_warn {
($mutex:expr, $name:expr) => {
$mutex.lock().unwrap_or_else(|e| {
warn_event!(
registry = $name,
"mutex was poisoned — recovering inner guard"
);
e.into_inner()
})
};
}
#[derive(Debug)]
pub struct EasyID3 {
pub id3: crate::id3::tags::ID3Tags,
pub filename: Option<String>,
trait_cache: HashMap<String, Vec<String>>,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum EasyID3Error {
#[error("Invalid key: {0}")]
InvalidKey(String),
#[error("Key not found: {0}")]
KeyNotFound(String),
#[error("Pattern matching error: {0}")]
PatternError(String),
#[error("Frame operation error: {0}")]
FrameError(String),
}
type GetterFn = Box<dyn Fn(&ID3Tags, &str) -> Result<Vec<String>> + Send + Sync>;
type SetterFn = Box<dyn Fn(&mut ID3Tags, &str, &[String]) -> Result<()> + Send + Sync>;
type DeleterFn = Box<dyn Fn(&mut ID3Tags, &str) -> Result<()> + Send + Sync>;
struct PatternMatcher {
matchers: Vec<(String, GlobMatcher)>,
}
impl PatternMatcher {
fn new() -> Self {
Self {
matchers: Vec::new(),
}
}
fn add_pattern(&mut self, pattern: &str) -> Result<()> {
let glob = Glob::new(pattern).map_err(|e| {
AudexError::InvalidData(format!("Invalid glob pattern {}: {}", pattern, e))
})?;
self.matchers
.push((pattern.to_string(), glob.compile_matcher()));
Ok(())
}
fn matches(&self, key: &str) -> Option<&str> {
for (pattern, matcher) in &self.matchers {
if matcher.is_match(key) {
return Some(pattern);
}
}
None
}
}
const MAX_REGISTRY_ENTRIES: usize = 1024;
static GET_REGISTRY: LazyLock<Mutex<HashMap<String, GetterFn>>> = LazyLock::new(|| {
let mut registry = HashMap::new();
register_standard_keys(&mut registry);
Mutex::new(registry)
});
static SET_REGISTRY: LazyLock<Mutex<HashMap<String, SetterFn>>> = LazyLock::new(|| {
let mut registry = HashMap::new();
register_standard_setters(&mut registry);
Mutex::new(registry)
});
static DELETE_REGISTRY: LazyLock<Mutex<HashMap<String, DeleterFn>>> = LazyLock::new(|| {
let mut registry = HashMap::new();
register_standard_deleters(&mut registry);
Mutex::new(registry)
});
static PATTERN_MATCHER: LazyLock<std::sync::Mutex<PatternMatcher>> = LazyLock::new(|| {
let mut matcher = PatternMatcher::new();
let _ = matcher.add_pattern("performer:*");
let _ = matcher.add_pattern("replaygain_*_gain");
let _ = matcher.add_pattern("replaygain_*_peak");
let _ = matcher.add_pattern("musicbrainz_*");
let _ = matcher.add_pattern("website:*");
std::sync::Mutex::new(matcher)
});
impl EasyID3 {
pub fn new() -> Self {
let mut easy = Self {
id3: crate::id3::tags::ID3Tags::new(),
filename: None,
trait_cache: HashMap::new(),
};
easy.refresh_trait_cache();
easy
}
fn refresh_trait_cache(&mut self) {
let keys = self.keys();
let mut cache = HashMap::with_capacity(keys.len());
for key in keys {
if let Some(values) = EasyID3::get(self, &key) {
cache.insert(key, values);
}
}
self.trait_cache = cache;
}
pub fn keys(&self) -> Vec<String> {
let mut keys = Vec::new();
{
let tags = &self.id3;
let registry = lock_or_warn!(GET_REGISTRY, "GET_REGISTRY");
for (key, getter) in registry.iter() {
if let Ok(values) = getter(tags, key) {
if !values.is_empty() {
keys.push(key.clone());
}
}
}
if let Some(txxx_frames) = tags.get_frames("TXXX") {
for frame in txxx_frames {
if let Some((desc, _)) = self.extract_txxx_content(frame) {
let key = desc.to_lowercase();
if !keys.contains(&key) {
keys.push(key);
}
}
}
}
for (role, _) in Self::read_tmcl_entries(tags) {
if !role.is_empty() {
let key = format!("performer:{}", role.to_lowercase());
if !keys.contains(&key) {
keys.push(key);
}
}
}
}
keys.sort();
keys.dedup();
keys
}
pub fn contains_key(&self, key: &str) -> bool {
let key_lower = key.to_lowercase();
let registry = lock_or_warn!(GET_REGISTRY, "GET_REGISTRY");
if registry.contains_key(&key_lower) {
let tags = &self.id3;
if let Some(getter) = registry.get(&key_lower) {
if let Ok(values) = getter(tags, &key_lower) {
return !values.is_empty();
}
}
}
drop(registry);
let matcher = lock_or_warn!(PATTERN_MATCHER, "PATTERN_MATCHER");
if let Some(pattern) = matcher.matches(&key_lower) {
let tags = &self.id3;
if let Some(values) = self.handle_pattern_get(tags, &key_lower, pattern) {
return !values.is_empty();
}
}
false
}
pub fn get(&self, key: &str) -> Option<Vec<String>> {
trace_event!(key = %key, "EasyID3 get");
let key_lower = key.to_lowercase();
{
let tags = &self.id3;
let registry = lock_or_warn!(GET_REGISTRY, "GET_REGISTRY");
if let Some(getter) = registry.get(&key_lower) {
if let Ok(values) = getter(tags, &key_lower) {
if !values.is_empty() {
return Some(values);
}
}
}
drop(registry);
let matcher = lock_or_warn!(PATTERN_MATCHER, "PATTERN_MATCHER");
if let Some(pattern) = matcher.matches(&key_lower) {
return self.handle_pattern_get(tags, &key_lower, pattern);
}
drop(matcher);
self.fallback_get(tags, &key_lower)
}
}
pub fn set(&mut self, key: &str, values: &[String]) -> Result<()> {
trace_event!(key = %key, count = values.len(), "EasyID3 set");
let key_lower = key.to_lowercase();
let result = {
let tags = &mut self.id3;
let registry = lock_or_warn!(SET_REGISTRY, "SET_REGISTRY");
if let Some(setter) = registry.get(&key_lower) {
setter(tags, &key_lower, values)
} else {
drop(registry);
let matcher = lock_or_warn!(PATTERN_MATCHER, "PATTERN_MATCHER");
if let Some(pattern) = matcher.matches(&key_lower) {
EasyID3::handle_pattern_set_static(tags, &key_lower, pattern, values)
} else {
drop(matcher);
EasyID3::fallback_set_static(tags, &key_lower, values)
}
}
};
if result.is_ok() {
self.refresh_trait_cache();
}
result
}
pub fn remove(&mut self, key: &str) -> Result<()> {
let key_lower = key.to_lowercase();
let result = {
let tags = &mut self.id3;
let registry = lock_or_warn!(DELETE_REGISTRY, "DELETE_REGISTRY");
if let Some(deleter) = registry.get(&key_lower) {
deleter(tags, &key_lower)
} else {
drop(registry);
let matcher = lock_or_warn!(PATTERN_MATCHER, "PATTERN_MATCHER");
if let Some(pattern) = matcher.matches(&key_lower) {
EasyID3::handle_pattern_delete_static(tags, &key_lower, pattern)
} else {
drop(matcher);
EasyID3::fallback_delete_static(tags, &key_lower)
}
}
};
if result.is_ok() {
self.refresh_trait_cache();
}
result
}
pub fn register_text_key(&mut self, key: &str, frame_id: &str) -> Result<()> {
debug_event!(key = %key, frame_id = %frame_id, "registered EasyID3 text key");
let key_lower = key.to_lowercase();
let frame_id = frame_id.to_string();
let getter_frame_id = frame_id.clone();
let getter: GetterFn = Box::new(move |tags, _key| {
if let Some(text_values) = tags.get_text_values(&getter_frame_id) {
Ok(text_values)
} else {
Ok(vec![])
}
});
let setter_frame_id = frame_id.clone();
let setter: SetterFn = Box::new(move |tags, _key, values| {
if values.is_empty() {
tags.remove(&setter_frame_id);
} else {
tags.remove(&setter_frame_id);
tags.add_text_frame(&setter_frame_id, values.to_vec())?;
}
Ok(())
});
let deleter_frame_id = frame_id.clone();
let deleter: DeleterFn = Box::new(move |tags, _key| {
tags.remove(&deleter_frame_id);
Ok(())
});
{
let mut get_reg = lock_or_warn!(GET_REGISTRY, "GET_REGISTRY");
let mut set_reg = lock_or_warn!(SET_REGISTRY, "SET_REGISTRY");
let mut del_reg = lock_or_warn!(DELETE_REGISTRY, "DELETE_REGISTRY");
if get_reg.contains_key(&key_lower)
|| set_reg.contains_key(&key_lower)
|| del_reg.contains_key(&key_lower)
{
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 key '{}' is already registered and cannot be replaced",
key
)));
}
if !get_reg.contains_key(&key_lower) && get_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
if !set_reg.contains_key(&key_lower) && set_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 SET registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
if !del_reg.contains_key(&key_lower) && del_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 DELETE registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
get_reg.insert(key_lower.clone(), getter);
set_reg.insert(key_lower.clone(), setter);
del_reg.insert(key_lower.clone(), deleter);
}
self.refresh_trait_cache();
Ok(())
}
pub fn register_txxx_key(&mut self, key: &str, description: &str) -> Result<()> {
debug_event!(key = %key, description = %description, "registered EasyID3 TXXX key");
let key_lower = key.to_lowercase();
let description = description.to_string();
let getter_desc = description.clone();
let getter: GetterFn = Box::new(move |tags, _key| {
if let Some(frames) = tags.get_frames("TXXX") {
for frame in frames {
if let Some(txxx) = frame.as_any().downcast_ref::<crate::id3::frames::TXXX>() {
if txxx.description.eq_ignore_ascii_case(&getter_desc) {
return Ok(txxx.text.clone());
}
}
}
}
Ok(vec![])
});
let setter_desc = description.clone();
let setter: SetterFn = Box::new(move |tags, _key, values| {
let pattern = format!("TXXX:{}", setter_desc);
tags.delall(&pattern);
if !values.is_empty() {
use crate::id3::{frames::TXXX, specs::TextEncoding};
let all_latin1 = setter_desc.chars().all(|c| (c as u32) <= 255)
&& values.iter().all(|v| v.chars().all(|c| (c as u32) <= 255));
let encoding = if all_latin1 {
TextEncoding::Latin1
} else {
TextEncoding::Utf16
};
let txxx_frame = TXXX::new(encoding, setter_desc.clone(), values.to_vec());
tags.add(Box::new(txxx_frame))?;
}
Ok(())
});
let deleter_desc = description.clone();
let deleter: DeleterFn = Box::new(move |tags, _key| {
let pattern = format!("TXXX:{}", deleter_desc);
tags.delall(&pattern);
Ok(())
});
{
let mut get_reg = lock_or_warn!(GET_REGISTRY, "GET_REGISTRY");
let mut set_reg = lock_or_warn!(SET_REGISTRY, "SET_REGISTRY");
let mut del_reg = lock_or_warn!(DELETE_REGISTRY, "DELETE_REGISTRY");
if !get_reg.contains_key(&key_lower) && get_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
if !set_reg.contains_key(&key_lower) && set_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 SET registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
if !del_reg.contains_key(&key_lower) && del_reg.len() >= MAX_REGISTRY_ENTRIES {
return Err(crate::AudexError::InvalidData(format!(
"EasyID3 DELETE registry limit reached ({} entries); cannot register key '{}'",
MAX_REGISTRY_ENTRIES, key
)));
}
get_reg.insert(key_lower.clone(), getter);
set_reg.insert(key_lower.clone(), setter);
del_reg.insert(key_lower.clone(), deleter);
}
Ok(())
}
pub fn pop(&mut self, key: &str) -> Option<Vec<String>> {
let values = self.get(key);
let _ = self.remove(key);
values
}
fn handle_pattern_get(&self, tags: &ID3Tags, key: &str, pattern: &str) -> Option<Vec<String>> {
match pattern {
"performer:*" => self.get_performer(tags, key),
"replaygain_*_gain" => self.get_replaygain_gain(tags, key),
"replaygain_*_peak" => self.get_replaygain_peak(tags, key),
"musicbrainz_*" => self.get_musicbrainz(tags, key),
"website:*" => self.get_website(tags, key),
_ => None,
}
}
fn handle_pattern_set_static(
tags: &mut ID3Tags,
key: &str,
pattern: &str,
values: &[String],
) -> Result<()> {
match pattern {
"performer:*" => Self::set_performer_static(tags, key, values),
"replaygain_*_gain" => Self::set_replaygain_gain_static(tags, key, values),
"replaygain_*_peak" => Self::set_replaygain_peak_static(tags, key, values),
"musicbrainz_*" => Self::set_musicbrainz_static(tags, key, values),
"website:*" => Self::set_website_static(tags, key, values),
_ => Err(AudexError::InvalidData(format!(
"Unknown pattern: {}",
pattern
))),
}
}
fn handle_pattern_delete_static(tags: &mut ID3Tags, key: &str, pattern: &str) -> Result<()> {
match pattern {
"performer:*" => Self::delete_performer_static(tags, key),
"replaygain_*_gain" => Self::delete_replaygain_gain_static(tags, key),
"replaygain_*_peak" => Self::delete_replaygain_peak_static(tags, key),
"musicbrainz_*" => Self::delete_musicbrainz_static(tags, key),
"website:*" => Self::delete_website_static(tags, key),
_ => Err(AudexError::InvalidData(format!(
"Unknown pattern: {}",
pattern
))),
}
}
fn fallback_get(&self, tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
self.get_txxx_frame(tags, key)
}
fn fallback_set_static(tags: &mut ID3Tags, key: &str, values: &[String]) -> Result<()> {
Self::set_txxx_frame_static(tags, key, values)
}
fn fallback_delete_static(tags: &mut ID3Tags, key: &str) -> Result<()> {
Self::delete_txxx_frame_static(tags, key)
}
fn get_performer(&self, tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
if let Some(role) = key.strip_prefix("performer:") {
self.get_performer_role(tags, role)
} else {
None
}
}
fn get_performer_role(&self, tags: &ID3Tags, role: &str) -> Option<Vec<String>> {
if let Some(frames) = tags.get_frames("TMCL") {
for frame in frames {
if let Some(text_values) = self.extract_text_from_frame(frame) {
let mut performers = Vec::new();
for i in (0..text_values.len()).step_by(2) {
if i + 1 < text_values.len() && text_values[i] == role {
performers.push(text_values[i + 1].clone());
}
}
if !performers.is_empty() {
return Some(performers);
}
}
}
}
None
}
fn get_replaygain_gain(&self, _tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
if let Some(track_type) = key.strip_prefix("replaygain_") {
if let Some(track_type) = track_type.strip_suffix("_gain") {
return self.get_rva2_gain(track_type);
}
}
None
}
fn get_replaygain_peak(&self, _tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
if let Some(track_type) = key.strip_prefix("replaygain_") {
if let Some(track_type) = track_type.strip_suffix("_peak") {
return self.get_rva2_peak(track_type);
}
}
None
}
fn get_musicbrainz(&self, tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
let txxx_desc = Self::musicbrainz_txxx_desc(key);
self.get_txxx_frame(tags, &txxx_desc)
}
fn get_website(&self, tags: &ID3Tags, key: &str) -> Option<Vec<String>> {
if key.starts_with("website:") {
if let Some(frames) = tags.get_frames("WOAR") {
let mut urls = Vec::new();
for frame in frames {
if let Some(url) = self.extract_url_from_frame(frame) {
urls.push(url);
}
}
if !urls.is_empty() {
return Some(urls);
}
}
}
None
}
fn get_txxx_frame(&self, tags: &ID3Tags, description: &str) -> Option<Vec<String>> {
if let Some(frames) = tags.get_frames("TXXX") {
for frame in frames {
if let Some((desc, value)) = self.extract_txxx_content(frame) {
if desc.to_uppercase() == description.to_uppercase() {
return Some(vec![value]);
}
}
}
}
None
}
fn get_rva2_gain(&self, track_type: &str) -> Option<Vec<String>> {
if let Some(tags) = self.tags() {
let key = format!("RVA2:{}", track_type);
if let Some(frame) = tags.get_frame(&key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
if let Some((gain, _)) = rva2.get_master() {
return Some(vec![format!("{:+.6} dB", gain)]);
}
}
}
}
None
}
fn get_rva2_peak(&self, track_type: &str) -> Option<Vec<String>> {
if let Some(tags) = self.tags() {
let key = format!("RVA2:{}", track_type);
if let Some(frame) = tags.get_frame(&key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
if let Some((_, peak)) = rva2.get_master() {
return Some(vec![format!("{:.6}", peak)]);
}
}
}
}
None
}
fn extract_text_from_frame(
&self,
frame: &dyn crate::id3::frames::Frame,
) -> Option<Vec<String>> {
if let Some(text_values) = frame.text_values() {
Some(text_values)
} else {
let desc = frame.description();
if let Some(colon_pos) = desc.find(": ") {
Some(vec![desc[colon_pos + 2..].to_string()])
} else {
None
}
}
}
fn extract_url_from_frame(&self, frame: &dyn crate::id3::frames::Frame) -> Option<String> {
let frame_id = frame.frame_id();
if frame_id.starts_with('W') {
let desc = frame.description();
desc.find(": ")
.map(|colon_pos| desc[colon_pos + 2..].to_string())
} else {
None
}
}
fn extract_txxx_content(
&self,
frame: &dyn crate::id3::frames::Frame,
) -> Option<(String, String)> {
if frame.frame_id() != "TXXX" {
return None;
}
if let Some(txxx) = frame.as_any().downcast_ref::<crate::id3::frames::TXXX>() {
let value = txxx.text.join("/");
return Some((txxx.description.clone(), value));
}
let description = frame.description();
if let Some(start) = description.find("TXXX: ") {
if let Some(equals_pos) = description[start + 6..].find(" = ") {
let desc_part = &description[start + 6..start + 6 + equals_pos];
let value_part = &description[start + 6 + equals_pos + 3..];
return Some((desc_part.to_string(), value_part.to_string()));
}
}
None
}
fn musicbrainz_txxx_desc(key: &str) -> String {
match key {
"musicbrainz_trackid" => "MusicBrainz Track Id".to_string(),
"musicbrainz_albumid" => "MusicBrainz Album Id".to_string(),
"musicbrainz_artistid" => "MusicBrainz Artist Id".to_string(),
"musicbrainz_albumartistid" => "MusicBrainz Album Artist Id".to_string(),
"musicbrainz_releasegroupid" => "MusicBrainz Release Group Id".to_string(),
"musicbrainz_workid" => "MusicBrainz Work Id".to_string(),
"musicbrainz_trmid" => "MusicBrainz TRM Id".to_string(),
"musicbrainz_discid" => "MusicBrainz Disc Id".to_string(),
"musicbrainz_albumstatus" => "MusicBrainz Album Status".to_string(),
"musicbrainz_albumtype" => "MusicBrainz Album Type".to_string(),
"musicbrainz_releasetrackid" => "MusicBrainz Release Track Id".to_string(),
other => other.to_uppercase(),
}
}
fn read_tmcl_entries(tags: &ID3Tags) -> Vec<(String, String)> {
let mut pairs = Vec::new();
if let Some(frames) = tags.get_frames("TMCL") {
for frame in frames {
if let Some(text_values) = frame.text_values() {
for i in (0..text_values.len()).step_by(2) {
if i + 1 < text_values.len() {
pairs.push((text_values[i].clone(), text_values[i + 1].clone()));
}
}
}
}
}
pairs
}
fn set_performer_static(tags: &mut ID3Tags, key: &str, values: &[String]) -> Result<()> {
let role = key.split(':').nth(1).unwrap_or("");
let mut existing_entries = Self::read_tmcl_entries(tags);
if !role.is_empty() {
existing_entries.retain(|(existing_role, _)| existing_role != role);
} else {
existing_entries.clear();
}
if !role.is_empty() {
for name in values {
existing_entries.push((role.to_string(), name.clone()));
}
} else {
for name in values {
existing_entries.push((String::new(), name.clone()));
}
}
if existing_entries.is_empty() {
tags.remove("TMCL");
} else {
let text_entries: Vec<String> = existing_entries
.into_iter()
.flat_map(|(role, performer)| vec![role, performer])
.collect();
tags.remove("TMCL");
tags.add_text_frame("TMCL", text_entries)?;
}
Ok(())
}
fn set_replaygain_gain_static(tags: &mut ID3Tags, key: &str, values: &[String]) -> Result<()> {
let track_type = if key.contains("track") {
"track"
} else if key.contains("album") {
"album"
} else {
return Err(AudexError::InvalidData(format!(
"Invalid replaygain gain key: {}. Must contain 'track' or 'album'",
key
)));
};
if values.is_empty() {
Self::delete_replaygain_gain_static(tags, key)?;
} else {
let gain_str = values[0].split_whitespace().next().unwrap_or(&values[0]);
let gain = gain_str
.parse::<f32>()
.map_err(|_| AudexError::InvalidData("Invalid gain value".to_string()))?;
if !gain.is_finite() {
return Err(AudexError::InvalidData(format!(
"Gain value must be finite, got: {}",
gain
)));
}
let frame_key = format!("RVA2:{}", track_type.to_uppercase());
let existing_peak = if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
rva2.get_master().map(|(_, peak)| peak)
} else {
None
}
} else {
None
};
tags.remove(&frame_key);
let peak = existing_peak.unwrap_or(0.0);
let rva2 = crate::id3::frames::RVA2::new(
track_type.to_uppercase(),
vec![(crate::id3::frames::ChannelType::MasterVolume, gain, peak)],
);
tags.add(Box::new(rva2))?;
}
Ok(())
}
fn set_replaygain_peak_static(tags: &mut ID3Tags, key: &str, values: &[String]) -> Result<()> {
let track_type = if key.contains("track") {
"track"
} else if key.contains("album") {
"album"
} else {
return Err(AudexError::InvalidData(format!(
"Invalid replaygain peak key: {}. Must contain 'track' or 'album'",
key
)));
};
if values.is_empty() {
Self::delete_replaygain_peak_static(tags, key)?;
} else {
let peak = values[0]
.parse::<f32>()
.map_err(|_| AudexError::InvalidData("Invalid peak value".to_string()))?;
if !(0.0..2.0).contains(&peak) {
return Err(AudexError::InvalidData(
"Peak must be >= 0 and < 2".to_string(),
));
}
let frame_key = format!("RVA2:{}", track_type.to_uppercase());
let existing_gain = if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
rva2.get_master().map(|(gain, _)| gain)
} else {
None
}
} else {
None
};
tags.remove(&frame_key);
let gain = existing_gain.unwrap_or(0.0);
let rva2 = crate::id3::frames::RVA2::new(
track_type.to_uppercase(),
vec![(crate::id3::frames::ChannelType::MasterVolume, gain, peak)],
);
tags.add(Box::new(rva2))?;
}
Ok(())
}
fn set_musicbrainz_static(tags: &mut ID3Tags, key: &str, values: &[String]) -> Result<()> {
let txxx_desc = Self::musicbrainz_txxx_desc(key);
Self::set_txxx_frame_static(tags, &txxx_desc, values)
}
fn set_website_static(tags: &mut ID3Tags, _key: &str, values: &[String]) -> Result<()> {
if values.is_empty() {
tags.remove("WOAR");
} else {
let website_text = values.join("/");
let frame = crate::id3::frames::TextFrame::single("WOAR".to_string(), website_text);
tags.add_text_frame(&frame.frame_id, frame.text)?;
}
Ok(())
}
fn set_txxx_frame_static(
tags: &mut ID3Tags,
description: &str,
values: &[String],
) -> Result<()> {
Self::delete_txxx_frame_static(tags, description)?;
if !values.is_empty() {
use crate::id3::{frames::TXXX, specs::TextEncoding};
let all_latin1 = description.chars().all(|c| (c as u32) <= 255)
&& values.iter().all(|v| v.chars().all(|c| (c as u32) <= 255));
let encoding = if all_latin1 {
TextEncoding::Latin1
} else {
TextEncoding::Utf16
};
let txxx_frame = TXXX::new(encoding, description.to_string(), values.to_vec());
tags.add(Box::new(txxx_frame))?;
}
Ok(())
}
fn delete_performer_static(tags: &mut ID3Tags, key: &str) -> Result<()> {
let role = key.split(':').nth(1).unwrap_or("");
if role.is_empty() {
tags.remove("TMCL");
return Ok(());
}
let existing_entries = Self::read_tmcl_entries(tags);
let remaining: Vec<(String, String)> = existing_entries
.into_iter()
.filter(|(existing_role, _)| existing_role != role)
.collect();
tags.remove("TMCL");
if !remaining.is_empty() {
let text_entries: Vec<String> = remaining
.into_iter()
.flat_map(|(role, performer)| vec![role, performer])
.collect();
tags.add_text_frame("TMCL", text_entries)?;
}
Ok(())
}
fn delete_replaygain_gain_static(tags: &mut ID3Tags, key: &str) -> Result<()> {
let track_type = if key.contains("track") {
"TRACK"
} else if key.contains("album") {
"ALBUM"
} else {
"TRACK"
};
let frame_key = format!("RVA2:{}", track_type);
let existing_peak = if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
rva2.get_master().map(|(_, peak)| peak)
} else {
None
}
} else {
None
};
tags.remove(&frame_key);
if let Some(peak) = existing_peak {
if peak != 0.0 {
let rva2 = crate::id3::frames::RVA2::new(
track_type.to_string(),
vec![(crate::id3::frames::ChannelType::MasterVolume, 0.0, peak)],
);
tags.add(Box::new(rva2))?;
}
}
Ok(())
}
fn delete_replaygain_peak_static(tags: &mut ID3Tags, key: &str) -> Result<()> {
let track_type = if key.contains("track") {
"TRACK"
} else if key.contains("album") {
"ALBUM"
} else {
"TRACK"
};
let frame_key = format!("RVA2:{}", track_type);
let existing_gain = if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
rva2.get_master().map(|(gain, _)| gain)
} else {
None
}
} else {
None
};
tags.remove(&frame_key);
if let Some(gain) = existing_gain {
if gain != 0.0 {
let rva2 = crate::id3::frames::RVA2::new(
track_type.to_string(),
vec![(crate::id3::frames::ChannelType::MasterVolume, gain, 0.0)],
);
tags.add(Box::new(rva2))?;
}
}
Ok(())
}
fn delete_musicbrainz_static(tags: &mut ID3Tags, key: &str) -> Result<()> {
let txxx_desc = Self::musicbrainz_txxx_desc(key);
Self::delete_txxx_frame_static(tags, &txxx_desc)
}
fn delete_website_static(tags: &mut ID3Tags, _key: &str) -> Result<()> {
tags.remove("WOAR");
Ok(())
}
fn delete_txxx_frame_static(tags: &mut ID3Tags, description: &str) -> Result<()> {
let pattern = format!("TXXX:{}", description);
tags.delall(&pattern);
Ok(())
}
pub fn debug_tags(&self) -> &crate::id3::tags::ID3Tags {
&self.id3
}
pub fn tags(&self) -> Option<&ID3Tags> {
Some(&self.id3)
}
pub fn save_to_file(&mut self) -> Result<()> {
if let Some(filename) = &self.filename {
self.id3.save(filename, 1, 4, None, None)
} else {
Err(AudexError::InvalidData(
"No filename stored for saving".to_string(),
))
}
}
#[cfg(feature = "async")]
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
use crate::id3::tags::ID3Tags;
let path_str = path.as_ref().to_string_lossy().to_string();
info_event!(path = %path_str, "loading EasyID3 tags (async)");
let id3_file = crate::id3::ID3::load_from_file_async(&path).await?;
let id3_tags = id3_file.tags().cloned().unwrap_or_else(ID3Tags::new);
let mut easy = Self {
id3: id3_tags,
filename: Some(path_str),
trait_cache: HashMap::new(),
};
easy.refresh_trait_cache();
Ok(easy)
}
#[cfg(feature = "async")]
pub async fn save_async(&mut self) -> Result<()> {
let filename = self
.filename
.clone()
.ok_or_else(|| AudexError::InvalidData("No filename set".to_string()))?;
let config = crate::id3::tags::ID3SaveConfig {
v2_version: 4,
v2_minor: 0,
v23_sep: "/".to_string(),
v23_separator: b'/',
padding: None,
merge_frames: true,
preserve_unknown: true,
compress_frames: false,
write_v1: crate::id3::file::ID3v1SaveOptions::CREATE,
unsync: false,
extended_header: false,
convert_v24_frames: true,
};
self.id3.save_to_file_async(&filename, &config).await
}
#[cfg(feature = "async")]
pub async fn clear_async(&mut self) -> Result<()> {
self.id3.dict.clear();
self.id3.frames_by_id.clear();
self.save_async().await
}
#[cfg(feature = "async")]
pub async fn delete_async(&mut self) -> Result<()> {
if let Some(filename) = &self.filename {
tokio::fs::remove_file(filename).await?;
self.filename = None;
}
Ok(())
}
}
impl Default for EasyID3 {
fn default() -> Self {
Self::new()
}
}
fn register_standard_keys(registry: &mut HashMap<String, GetterFn>) {
register_text_key(registry, "album", "TALB");
register_tpe_key(registry, "albumartist", "TPE2"); register_text_key(registry, "albumartistsort", "TSO2");
register_text_key(registry, "albumsort", "TSOA");
register_text_key(registry, "arranger", "TPE4");
register_tpe_key(registry, "artist", "TPE1"); register_text_key(registry, "artistsort", "TSOP");
register_text_key(registry, "author", "TOLY");
register_text_key(registry, "bpm", "TBPM");
register_text_key(registry, "composer", "TCOM");
register_text_key(registry, "composersort", "TSOC");
register_text_key(registry, "conductor", "TPE3");
register_text_key(registry, "copyright", "TCOP");
register_date_key(registry);
register_text_key(registry, "discnumber", "TPOS");
register_text_key(registry, "discsubtitle", "TSST");
register_text_key(registry, "encodedby", "TENC");
register_text_key(registry, "encodersettings", "TSSE");
register_text_key(registry, "fileowner", "TOWN");
register_genre_key(registry);
register_text_key(registry, "grouping", "TIT1");
register_text_key(registry, "isrc", "TSRC");
register_text_key(registry, "language", "TLAN");
register_text_key(registry, "length", "TLEN");
register_text_key(registry, "lyricist", "TEXT");
register_text_key(registry, "media", "TMED");
register_text_key(registry, "mood", "TMOO");
register_text_key(registry, "organization", "TPUB");
register_text_key(registry, "originalalbum", "TOAL");
register_text_key(registry, "originalartist", "TOPE");
register_text_key(registry, "originaldate", "TDOR");
register_text_key(registry, "title", "TIT2");
register_text_key(registry, "titlesort", "TSOT");
register_text_key(registry, "compilation", "TCMP");
register_text_key(registry, "tracknumber", "TRCK");
register_text_key(registry, "version", "TIT3");
register_comment_key(registry);
let website_getter: GetterFn = Box::new(|tags, _key| {
if let Some(frames) = tags.get_frames("WOAR") {
let mut urls = Vec::new();
for frame in frames {
if let Some(text_values) = frame.text_values() {
urls.extend(text_values);
}
}
if !urls.is_empty() {
return Ok(urls);
}
}
Ok(vec![])
});
registry.insert("website".to_string(), website_getter);
register_txxx_key(registry, "acoustid_fingerprint", "Acoustid Fingerprint");
register_txxx_key(registry, "acoustid_id", "Acoustid Id");
register_txxx_key(
registry,
"musicbrainz_albumartistid",
"MusicBrainz Album Artist Id",
);
register_txxx_key(registry, "musicbrainz_albumid", "MusicBrainz Album Id");
register_txxx_key(
registry,
"musicbrainz_albumstatus",
"MusicBrainz Album Status",
);
register_txxx_key(registry, "musicbrainz_albumtype", "MusicBrainz Album Type");
register_txxx_key(registry, "musicbrainz_artistid", "MusicBrainz Artist Id");
register_txxx_key(registry, "musicbrainz_discid", "MusicBrainz Disc Id");
register_txxx_key(
registry,
"musicbrainz_releasegroupid",
"MusicBrainz Release Group Id",
);
register_txxx_key(
registry,
"musicbrainz_releasetrackid",
"MusicBrainz Release Track Id",
);
let ufid_getter: GetterFn = Box::new(|tags, _key| {
let frames = tags.getall("UFID:http://musicbrainz.org");
if let Some(frame) = frames.first() {
if let Some(ufid) = frame.as_any().downcast_ref::<crate::id3::frames::UFID>() {
if let Ok(s) = String::from_utf8(ufid.data.clone()) {
return Ok(vec![s]);
}
}
}
Ok(vec![])
});
registry.insert("musicbrainz_trackid".to_string(), ufid_getter);
register_txxx_key(registry, "musicbrainz_trmid", "MusicBrainz TRM Id");
register_txxx_key(registry, "musicbrainz_workid", "MusicBrainz Work Id");
register_txxx_key(registry, "musicip_fingerprint", "MusicMagic Fingerprint");
register_txxx_key(registry, "musicip_puid", "MusicIP PUID");
register_txxx_key(
registry,
"releasecountry",
"MusicBrainz Album Release Country",
);
register_txxx_key(registry, "asin", "ASIN");
register_txxx_key(registry, "barcode", "BARCODE");
register_txxx_key(registry, "catalognumber", "CATALOGNUMBER");
register_txxx_key(registry, "performer", "PERFORMER");
registry.insert(
"replaygain_*_gain".to_string(),
Box::new(|tags, key| {
if let Some(track_type) = key.strip_prefix("replaygain_") {
if let Some(track_type) = track_type.strip_suffix("_gain") {
let frame_key = format!("RVA2:{}", track_type.to_uppercase());
if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) =
frame.as_any().downcast_ref::<crate::id3::frames::RVA2>()
{
if let Some((gain, _)) = rva2.get_master() {
return Ok(vec![format!("{:+.6} dB", gain)]);
}
}
}
}
}
Err(AudexError::InvalidData(format!("Key not found: {}", key)))
}),
);
registry.insert(
"replaygain_*_peak".to_string(),
Box::new(|tags, key| {
if let Some(track_type) = key.strip_prefix("replaygain_") {
if let Some(track_type) = track_type.strip_suffix("_peak") {
let frame_key = format!("RVA2:{}", track_type.to_uppercase());
if let Some(frame) = tags.get_frame(&frame_key) {
if let Some(rva2) =
frame.as_any().downcast_ref::<crate::id3::frames::RVA2>()
{
if let Some((_, peak)) = rva2.get_master() {
return Ok(vec![format!("{:.6}", peak)]);
}
}
}
}
}
Err(AudexError::InvalidData(format!("Key not found: {}", key)))
}),
);
}
fn register_standard_setters(registry: &mut HashMap<String, SetterFn>) {
register_text_setter(registry, "album", "TALB");
register_tpe_setter(registry, "albumartist", "TPE2"); register_text_setter(registry, "albumartistsort", "TSO2");
register_text_setter(registry, "albumsort", "TSOA");
register_text_setter(registry, "arranger", "TPE4");
register_tpe_setter(registry, "artist", "TPE1"); register_text_setter(registry, "artistsort", "TSOP");
register_text_setter(registry, "author", "TOLY");
register_text_setter(registry, "bpm", "TBPM");
register_text_setter(registry, "composer", "TCOM");
register_text_setter(registry, "composersort", "TSOC");
register_text_setter(registry, "conductor", "TPE3");
register_text_setter(registry, "copyright", "TCOP");
register_date_setter(registry);
register_text_setter(registry, "discnumber", "TPOS");
register_text_setter(registry, "discsubtitle", "TSST");
register_text_setter(registry, "encodedby", "TENC");
register_text_setter(registry, "encodersettings", "TSSE");
register_text_setter(registry, "fileowner", "TOWN");
register_genre_setter(registry);
register_text_setter(registry, "grouping", "TIT1");
register_text_setter(registry, "isrc", "TSRC");
register_text_setter(registry, "language", "TLAN");
register_text_setter(registry, "length", "TLEN");
register_text_setter(registry, "lyricist", "TEXT");
register_text_setter(registry, "media", "TMED");
register_text_setter(registry, "mood", "TMOO");
register_text_setter(registry, "organization", "TPUB");
register_text_setter(registry, "originalalbum", "TOAL");
register_text_setter(registry, "originalartist", "TOPE");
register_text_setter(registry, "originaldate", "TDOR");
register_text_setter(registry, "title", "TIT2");
register_text_setter(registry, "titlesort", "TSOT");
register_text_setter(registry, "compilation", "TCMP");
register_text_setter(registry, "tracknumber", "TRCK");
register_text_setter(registry, "version", "TIT3");
register_comment_setter(registry);
register_txxx_setter(registry, "acoustid_fingerprint", "Acoustid Fingerprint");
register_txxx_setter(registry, "acoustid_id", "Acoustid Id");
register_txxx_setter(
registry,
"musicbrainz_albumartistid",
"MusicBrainz Album Artist Id",
);
register_txxx_setter(registry, "musicbrainz_albumid", "MusicBrainz Album Id");
register_txxx_setter(
registry,
"musicbrainz_albumstatus",
"MusicBrainz Album Status",
);
register_txxx_setter(registry, "musicbrainz_albumtype", "MusicBrainz Album Type");
register_txxx_setter(registry, "musicbrainz_artistid", "MusicBrainz Artist Id");
register_txxx_setter(registry, "musicbrainz_discid", "MusicBrainz Disc Id");
register_txxx_setter(
registry,
"musicbrainz_releasegroupid",
"MusicBrainz Release Group Id",
);
register_txxx_setter(
registry,
"musicbrainz_releasetrackid",
"MusicBrainz Release Track Id",
);
let ufid_setter: SetterFn = Box::new(|tags, _key, values| {
if values.len() != 1 {
return Err(crate::AudexError::InvalidData(
"only one track ID may be set per song".to_string(),
));
}
tags.delall("UFID:http://musicbrainz.org");
let ufid = crate::id3::frames::UFID::new(
"http://musicbrainz.org".to_string(),
values[0].as_bytes().to_vec(),
);
tags.add(Box::new(ufid))?;
Ok(())
});
registry.insert("musicbrainz_trackid".to_string(), ufid_setter);
register_txxx_setter(registry, "musicbrainz_trmid", "MusicBrainz TRM Id");
register_txxx_setter(registry, "musicbrainz_workid", "MusicBrainz Work Id");
register_txxx_setter(registry, "musicip_fingerprint", "MusicMagic Fingerprint");
register_txxx_setter(registry, "musicip_puid", "MusicIP PUID");
register_txxx_setter(
registry,
"releasecountry",
"MusicBrainz Album Release Country",
);
register_txxx_setter(registry, "asin", "ASIN");
register_txxx_setter(registry, "barcode", "BARCODE");
register_txxx_setter(registry, "catalognumber", "CATALOGNUMBER");
register_txxx_setter(registry, "performer", "PERFORMER");
registry.insert(
"replaygain_*_gain".to_string(),
Box::new(EasyID3::set_replaygain_gain_static),
);
registry.insert(
"replaygain_*_peak".to_string(),
Box::new(EasyID3::set_replaygain_peak_static),
);
}
fn register_standard_deleters(registry: &mut HashMap<String, DeleterFn>) {
register_text_deleter(registry, "album", "TALB");
register_text_deleter(registry, "albumartist", "TPE2");
register_text_deleter(registry, "artist", "TPE1");
register_text_deleter(registry, "title", "TIT2");
register_text_deleter(registry, "genre", "TCON");
register_date_deleter(registry);
register_text_deleter(registry, "originaldate", "TDOR");
register_text_deleter(registry, "tracknumber", "TRCK");
register_text_deleter(registry, "discnumber", "TPOS");
register_text_deleter(registry, "composer", "TCOM");
register_text_deleter(registry, "conductor", "TPE3");
register_text_deleter(registry, "copyright", "TCOP");
register_text_deleter(registry, "encodedby", "TENC");
register_text_deleter(registry, "grouping", "TIT1");
register_text_deleter(registry, "lyricist", "TEXT");
register_text_deleter(registry, "mood", "TMOO");
register_text_deleter(registry, "organization", "TPUB");
register_text_deleter(registry, "compilation", "TCMP");
register_text_deleter(registry, "originalalbum", "TOAL");
register_text_deleter(registry, "originalartist", "TOPE");
register_comment_deleter(registry);
let ufid_deleter: DeleterFn = Box::new(|tags, _key| {
tags.delall("UFID:http://musicbrainz.org");
Ok(())
});
registry.insert("musicbrainz_trackid".to_string(), ufid_deleter);
}
fn register_text_deleter(registry: &mut HashMap<String, DeleterFn>, key: &str, frame_id: &str) {
let frame_id = frame_id.to_string();
let deleter: DeleterFn = Box::new(move |tags, _key| {
tags.remove(&frame_id);
Ok(())
});
registry.insert(key.to_string(), deleter);
}
fn register_text_key(registry: &mut HashMap<String, GetterFn>, key: &str, frame_id: &str) {
let frame_id = frame_id.to_string();
let getter: GetterFn = Box::new(move |tags, _key| {
if let Some(text_values) = tags.get_text_values(&frame_id) {
Ok(text_values)
} else {
Ok(vec![])
}
});
registry.insert(key.to_string(), getter);
}
fn register_tpe_key(registry: &mut HashMap<String, GetterFn>, key: &str, frame_id: &str) {
let frame_id = frame_id.to_string();
let getter: GetterFn = Box::new(move |tags, _key| {
if let Some(text_values) = tags.get_text_values(&frame_id) {
if !text_values.is_empty() {
return Ok(text_values);
}
}
if let Some(frames) = tags.get_frames(&frame_id) {
for frame in frames {
if let Some(text_values) = frame.text_values() {
if !text_values.is_empty() {
return Ok(text_values);
}
}
let desc = frame.description();
if let Some(colon_pos) = desc.find(": ") {
let value = desc[colon_pos + 2..].trim().to_string();
if !value.is_empty() {
return Ok(vec![value]);
}
}
}
}
if let Some(text) = tags.get_text(&frame_id) {
if !text.is_empty() {
return Ok(vec![text]);
}
}
Ok(vec![])
});
registry.insert(key.to_string(), getter);
}
fn register_text_setter(registry: &mut HashMap<String, SetterFn>, key: &str, frame_id: &str) {
let frame_id = frame_id.to_string();
let setter: SetterFn = Box::new(move |tags, _key, values| {
if values.is_empty() {
tags.remove(&frame_id);
} else {
tags.remove(&frame_id);
tags.add_text_frame(&frame_id, values.to_vec())?;
}
Ok(())
});
registry.insert(key.to_string(), setter);
}
fn register_tpe_setter(registry: &mut HashMap<String, SetterFn>, key: &str, frame_id: &str) {
let frame_id = frame_id.to_string();
let setter: SetterFn = Box::new(move |tags, _key, values| {
if values.is_empty() {
tags.remove(&frame_id);
} else {
tags.remove(&frame_id);
match tags.add_text_frame(&frame_id, values.to_vec()) {
Ok(()) => {
if tags.get_frames(&frame_id).is_none() {
let joined_value = values.join("/");
tags.set_text(&frame_id, joined_value)?;
}
return Ok(());
}
Err(_) => {
let joined_value = values.join("/");
tags.set_text(&frame_id, joined_value)?;
}
}
}
Ok(())
});
registry.insert(key.to_string(), setter);
}
fn register_txxx_key(registry: &mut HashMap<String, GetterFn>, key: &str, txxx_desc: &str) {
let desc = txxx_desc.to_string();
let getter: GetterFn = Box::new(move |tags, _key| {
if let Some(frames) = tags.get_frames("TXXX") {
for frame in frames {
if let Some(txxx) = frame.as_any().downcast_ref::<crate::id3::frames::TXXX>() {
if txxx.description.eq_ignore_ascii_case(&desc) {
return Ok(txxx.text.clone());
}
}
}
}
Ok(vec![])
});
registry.insert(key.to_string(), getter);
}
fn register_txxx_setter(registry: &mut HashMap<String, SetterFn>, key: &str, txxx_desc: &str) {
let desc = txxx_desc.to_string();
let setter: SetterFn = Box::new(move |tags, _key, values| {
let pattern = format!("TXXX:{}", desc);
tags.delall(&pattern);
if !values.is_empty() {
use crate::id3::{frames::TXXX, specs::TextEncoding};
let all_latin1 = desc.chars().all(|c| (c as u32) <= 255)
&& values.iter().all(|v| v.chars().all(|c| (c as u32) <= 255));
let encoding = if all_latin1 {
TextEncoding::Latin1
} else {
TextEncoding::Utf16
};
let txxx_frame = TXXX::new(encoding, desc.clone(), values.to_vec());
tags.add(Box::new(txxx_frame))?;
}
Ok(())
});
registry.insert(key.to_string(), setter);
}
fn register_genre_key(registry: &mut HashMap<String, GetterFn>) {
let getter: GetterFn = Box::new(|tags, _key| {
if let Some(text_values) = tags.get_text_values("TCON") {
let mut all_genres = Vec::new();
for text in text_values {
all_genres.extend(parse_genre_text(&text));
}
Ok(all_genres)
} else {
Ok(vec![])
}
});
registry.insert("genre".to_string(), getter);
}
fn register_genre_setter(registry: &mut HashMap<String, SetterFn>) {
let setter: SetterFn = Box::new(|tags, _key, values| {
if values.is_empty() {
tags.remove("TCON");
} else {
let genre_text = values.join("\0");
tags.set_text("TCON", genre_text)?;
}
Ok(())
});
registry.insert("genre".to_string(), setter);
}
fn register_comment_key(registry: &mut HashMap<String, GetterFn>) {
let getter: GetterFn = Box::new(|tags, _key| {
if let Some(frames) = tags.get_frames("COMM") {
for frame in &frames {
if let Some(comm) = frame.as_any().downcast_ref::<crate::id3::frames::COMM>() {
if comm.description.is_empty() || comm.description == "ID3v1 Comment" {
return Ok(vec![comm.text.clone()]);
}
}
}
if let Some(frame) = frames.first() {
if let Some(comm) = frame.as_any().downcast_ref::<crate::id3::frames::COMM>() {
return Ok(vec![comm.text.clone()]);
}
}
}
Ok(vec![])
});
registry.insert("comment".to_string(), getter);
}
fn register_comment_setter(registry: &mut HashMap<String, SetterFn>) {
let setter: SetterFn = Box::new(|tags, _key, values| {
tags.remove("COMM");
if !values.is_empty() {
use crate::id3::{frames::COMM, specs::TextEncoding};
let text = values[0].clone();
let all_latin1 = text.chars().all(|c| (c as u32) <= 255);
let encoding = if all_latin1 {
TextEncoding::Latin1
} else {
TextEncoding::Utf16
};
let comm = COMM::new(encoding, *b"eng", String::new(), text);
tags.add(Box::new(comm))?;
}
Ok(())
});
registry.insert("comment".to_string(), setter);
}
fn register_comment_deleter(registry: &mut HashMap<String, DeleterFn>) {
let deleter: DeleterFn = Box::new(|tags, _key| {
tags.remove("COMM");
Ok(())
});
registry.insert("comment".to_string(), deleter);
}
fn register_date_key(registry: &mut HashMap<String, GetterFn>) {
let getter: GetterFn = Box::new(|tags, _key| {
if let Some(text) = tags.get_text("TDRC") {
Ok(vec![text])
} else if let Some(text) = tags.get_text("TYER") {
Ok(vec![text])
} else if let Some(text) = tags.get_text("TDAT") {
Ok(vec![text])
} else {
Ok(vec![])
}
});
registry.insert("date".to_string(), getter);
}
fn register_date_setter(registry: &mut HashMap<String, SetterFn>) {
let setter: SetterFn = Box::new(|tags, _key, values| {
if values.is_empty() {
tags.remove("TDRC");
tags.remove("TYER");
tags.remove("TDAT");
} else {
let date_text = &values[0];
tags.set_text("TDRC", date_text.clone())?;
tags.remove("TYER");
tags.remove("TDAT");
}
Ok(())
});
registry.insert("date".to_string(), setter);
}
fn register_date_deleter(registry: &mut HashMap<String, DeleterFn>) {
let deleter: DeleterFn = Box::new(|tags, _key| {
tags.remove("TDRC");
tags.remove("TYER");
tags.remove("TDAT");
tags.remove("TIME");
Ok(())
});
registry.insert("date".to_string(), deleter);
}
fn parse_genre_text(text: &str) -> Vec<String> {
let mut genres = Vec::new();
if text.starts_with('(') {
let mut chars = text.chars().peekable();
let mut current = String::new();
let mut in_parens = false;
while let Some(c) = chars.next() {
match c {
'(' => {
in_parens = true;
current.clear();
}
')' => {
if in_parens {
if let Ok(genre_num) = current.parse::<u8>() {
if let Some(genre_name) = crate::constants::get_genre(genre_num) {
genres.push(genre_name.to_string());
} else {
genres.push(genre_num.to_string());
}
} else if !current.is_empty() {
genres.push(current.clone());
}
in_parens = false;
current.clear();
}
}
_ => {
if in_parens {
current.push(c);
} else if !c.is_whitespace() {
current.push(c);
let rest: String = chars.collect();
current.push_str(&rest);
genres.push(current.trim().to_string());
break;
}
}
}
}
} else {
for genre in text.split('\0') {
let genre = genre.trim();
if !genre.is_empty() {
genres.push(genre.to_string());
}
}
}
if genres.is_empty() && !text.is_empty() {
genres.push(text.to_string());
}
genres
}
impl From<EasyID3Error> for AudexError {
fn from(err: EasyID3Error) -> Self {
AudexError::InvalidData(err.to_string())
}
}
impl crate::tags::Tags for EasyID3 {
fn get(&self, key: &str) -> Option<&[String]> {
self.trait_cache.get(&key.to_lowercase()).map(Vec::as_slice)
}
fn set(&mut self, key: &str, values: Vec<String>) {
let _ = EasyID3::set(self, key, &values);
}
fn remove(&mut self, key: &str) {
let _ = EasyID3::remove(self, key);
}
fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self.trait_cache.keys().cloned().collect();
keys.sort();
keys
}
fn pprint(&self) -> String {
let mut result = String::new();
for key in crate::tags::Tags::keys(self) {
if let Some(values) = crate::tags::Tags::get(self, &key) {
result.push_str(&format!("{}: {:?}\n", key, values));
}
}
result
}
}
impl FileType for EasyID3 {
type Tags = ID3Tags;
type Info = crate::id3::file::EmptyStreamInfo;
fn format_id() -> &'static str {
"EasyID3"
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(format = "EasyID3"))
)]
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_str = path.as_ref().to_string_lossy().to_string();
info_event!(path = %path_str, "loading EasyID3 tags");
let id3_file = crate::id3::ID3::load_from_file(&path)?;
let id3_tags = id3_file.tags().cloned().unwrap_or_else(ID3Tags::new);
let mut easy = Self {
id3: id3_tags,
filename: Some(path_str),
trait_cache: HashMap::new(),
};
easy.refresh_trait_cache();
Ok(easy)
}
fn save(&mut self) -> Result<()> {
self.save_to_file()
}
fn clear(&mut self) -> Result<()> {
self.id3.dict.clear();
self.id3.frames_by_id.clear();
self.save()
}
fn add_tags(&mut self) -> Result<()> {
Err(AudexError::InvalidOperation(
"ID3 tags already exist".to_string(),
))
}
fn tags(&self) -> Option<&Self::Tags> {
Some(&self.id3)
}
fn tags_mut(&mut self) -> Option<&mut Self::Tags> {
Some(&mut self.id3)
}
fn info(&self) -> &Self::Info {
static EMPTY_INFO: crate::id3::file::EmptyStreamInfo = crate::id3::file::EmptyStreamInfo;
&EMPTY_INFO
}
fn score(filename: &str, header: &[u8]) -> i32 {
crate::id3::ID3::score(filename, header)
}
fn mime_types() -> &'static [&'static str] {
crate::id3::ID3::mime_types()
}
}