use crate::constants;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[non_exhaustive]
pub enum TagSystem {
ID3v2,
VorbisComment,
MP4,
APEv2,
ASF,
}
pub fn normalize_track_disc(raw: &str) -> (Option<String>, Option<String>) {
let trimmed = raw.trim();
if trimmed.is_empty() {
return (None, None);
}
let is_numeric = |s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_digit());
let strip_leading_zeros = |s: &str| -> String {
let stripped = s.trim_start_matches('0');
if stripped.is_empty() {
"0".to_string()
} else {
stripped.to_string()
}
};
if let Some((left, right)) = trimmed.split_once('/') {
let number_part = left.trim();
let total_part = right.trim();
let number = if is_numeric(number_part) {
Some(strip_leading_zeros(number_part))
} else {
None
};
let total = if is_numeric(total_part) {
Some(strip_leading_zeros(total_part))
} else {
None
};
(number, total)
} else if is_numeric(trimmed) {
(Some(strip_leading_zeros(trimmed)), None)
} else {
(None, None)
}
}
pub fn combine_track_disc(number: Option<&str>, total: Option<&str>) -> String {
match (number, total) {
(Some(n), Some(t)) => format!("{}/{}", n, t),
(Some(n), None) => n.to_string(),
(None, Some(t)) => format!("0/{}", t),
(None, None) => String::new(),
}
}
pub fn resolve_id3_genre(raw: &str) -> String {
trace_event!(raw = %raw, "resolving ID3 genre reference");
let trimmed = raw.trim();
if trimmed.is_empty() {
return String::new();
}
if trimmed.starts_with('(') {
let mut resolved: Vec<String> = Vec::new();
let mut rest = trimmed;
while let Some(after_open) = rest.strip_prefix('(') {
if let Some((num_str, remainder)) = after_open.split_once(')') {
if let Ok(genre_id) = num_str.parse::<u16>() {
let name = u8::try_from(genre_id)
.ok()
.and_then(|id| constants::get_genre(id))
.map(|s| s.to_string())
.unwrap_or_else(|| genre_id.to_string());
resolved.push(name);
rest = remainder;
continue;
}
}
break;
}
if !resolved.is_empty() {
let suffix = rest.trim();
if !suffix.is_empty() {
if let Some(last) = resolved.last_mut() {
*last = suffix.to_string();
}
}
return resolved.join("/");
}
return trimmed.to_string();
}
if let Ok(genre_id) = trimmed.parse::<u16>() {
if let Some(genre_name) = u8::try_from(genre_id)
.ok()
.and_then(|id| constants::get_genre(id))
{
return genre_name.to_string();
}
return trimmed.to_string();
}
trimmed.to_string()
}
pub fn normalize_date(raw: &str, _source_format: TagSystem) -> String {
trace_event!(raw = %raw, "normalizing date value");
let trimmed = raw.trim();
if trimmed.is_empty() {
return String::new();
}
trimmed.to_string()
}
pub fn normalize_boolean(raw: &str, _source_format: TagSystem) -> String {
trace_event!(raw = %raw, "normalizing boolean value");
let lower = raw.trim().to_lowercase();
match lower.as_str() {
"1" | "true" | "yes" => "1".to_string(),
"0" | "false" | "no" | "" => "0".to_string(),
_ => raw.trim().to_string(),
}
}