use crate::id3::{ID3, ID3Tags};
use crate::mp3::util::{BitrateMode, MPEGFrame};
use crate::mp3::{ChannelMode, Emphasis, MPEGLayer, MPEGVersion};
use crate::{AudexError, FileType, Result, StreamInfo};
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "async")]
use tokio::fs::File as TokioFile;
#[cfg(feature = "async")]
use tokio::io::{AsyncReadExt, AsyncSeekExt};
#[derive(Debug)]
pub struct MP3 {
pub info: MPEGInfo,
pub tags: Option<ID3Tags>,
pub filename: Option<String>,
}
impl MP3 {
fn get_rva2_replaygain(&self, track_type: &str, is_gain: bool) -> Option<Vec<String>> {
let tags = self.tags.as_ref()?;
let lower_type = track_type.to_lowercase();
for (key, frame) in tags.dict.iter() {
if !key.starts_with("RVA2") {
continue;
}
if !key.to_lowercase().contains(&lower_type) {
continue;
}
if let Some(rva2) = frame.as_any().downcast_ref::<crate::id3::frames::RVA2>() {
if let Some((gain, peak)) = rva2.get_master() {
return if is_gain {
let rounded = (gain * 100.0).round() / 100.0;
Some(vec![format!("{}", rounded)])
} else {
let rounded = (peak * 10000000.0).round() / 10000000.0;
Some(vec![format!("{}", rounded)])
};
}
}
}
None
}
pub fn new() -> Self {
Self {
info: MPEGInfo::default(),
tags: None,
filename: None,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all, fields(path = %path.as_ref().display())))]
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
debug_event!("parsing MP3 file");
let mut file = File::open(path)?;
let mut mp3 = Self::new();
mp3.filename = Some(path.to_string_lossy().to_string());
match ID3::load_from_file(path) {
Ok(id3) => {
debug_event!("ID3v2 tags parsed for MP3");
mp3.tags = Some(id3.tags);
}
Err(_) => {
mp3.tags = None;
}
}
mp3.info = MPEGInfo::from_file(&mut file)?;
debug_event!(
bitrate = mp3.info.bitrate,
sample_rate = mp3.info.sample_rate,
channels = mp3.info.channels,
"MPEG stream info parsed"
);
Ok(mp3)
}
pub fn save(&mut self) -> Result<()> {
debug_event!("saving MP3 tags");
self.save_with_options(None, None, None, None)
}
pub fn save_with_options(
&mut self,
file_path: Option<&str>,
v1: Option<u8>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let target_path = match file_path {
Some(path) => path,
None => self.filename.as_deref().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
let v1_option = v1.unwrap_or(2); let v2_version_option = v2_version.unwrap_or(3); let v23_sep_string = v23_sep.map(|s| s.to_string());
trace_event!(
path = target_path,
id3v1_option = v1_option,
id3v2_version = v2_version_option,
"writing MP3 ID3 tags to file"
);
if let Some(ref mut tags) = self.tags {
tags.save(
target_path,
v1_option,
v2_version_option,
v23_sep_string,
None,
)?;
}
Ok(())
}
}
#[cfg(feature = "async")]
impl MP3 {
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::from_file_async(path).await
}
pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let mut file = TokioFile::open(path).await?;
let mut mp3 = Self::new();
mp3.filename = Some(path.to_string_lossy().to_string());
match ID3::load_from_file_async(path).await {
Ok(id3) => {
mp3.tags = Some(id3.tags);
}
Err(_) => {
mp3.tags = None;
}
}
mp3.info = Self::parse_mpeg_info_async(&mut file).await?;
Ok(mp3)
}
async fn parse_mpeg_info_async(file: &mut TokioFile) -> Result<MPEGInfo> {
Self::skip_id3v2_async(file).await?;
let (frame, overall_sketchy) = Self::find_and_parse_frame_async(file).await?;
let mut info = MPEGInfo {
length: frame.length,
bitrate: frame.bitrate,
sample_rate: frame.sample_rate,
channels: frame.channels(),
version: frame.version,
layer: frame.layer,
channel_mode: frame.channel_mode,
emphasis: frame.emphasis,
protected: frame.protected,
padding: frame.padding,
private: frame.private,
copyright: frame.copyright,
original: frame.original,
mode_extension: frame.mode_extension,
sketchy: overall_sketchy,
bitrate_mode: frame.bitrate_mode,
encoder_info: frame.encoder_info,
encoder_settings: frame.encoder_settings,
track_gain: frame.track_gain,
track_peak: frame.track_peak,
album_gain: frame.album_gain,
album_peak: None,
};
if info.length.is_none() {
Self::estimate_length_async(&mut info, file, frame.frame_offset).await?;
}
Ok(info)
}
async fn skip_id3v2_async(reader: &mut TokioFile) -> Result<()> {
reader.seek(SeekFrom::Start(0)).await?;
let file_size = reader.seek(SeekFrom::End(0)).await?;
reader.seek(SeekFrom::Start(0)).await?;
const MAX_ID3_SKIP_ITERATIONS: usize = 1000;
let mut id3_iterations = 0usize;
loop {
id3_iterations += 1;
if id3_iterations > MAX_ID3_SKIP_ITERATIONS {
break;
}
let mut id3_header = [0u8; 10];
let mut bytes_read = 0usize;
while bytes_read < id3_header.len() {
let read_now = reader.read(&mut id3_header[bytes_read..]).await?;
if read_now == 0 {
break;
}
bytes_read += read_now;
}
if bytes_read < 10 {
reader.seek(SeekFrom::Start(0)).await?;
break;
}
if &id3_header[0..3] == b"ID3" {
let tag_size =
crate::id3::util::decode_synchsafe_int_checked(&id3_header[6..10])? as u64;
let current_pos = reader.stream_position().await?;
if tag_size > 0 && current_pos + tag_size <= file_size {
let skip = i64::try_from(tag_size).map_err(|_| {
AudexError::InvalidData("ID3 tag size exceeds i64 range".to_string())
})?;
reader.seek(SeekFrom::Current(skip)).await?;
continue;
}
}
reader.seek(SeekFrom::Current(-(bytes_read as i64))).await?;
break;
}
Ok(())
}
async fn find_and_parse_frame_async(reader: &mut TokioFile) -> Result<(MPEGFrame, bool)> {
const MAX_READ: u64 = 1024 * 1024; const MAX_SYNCS: usize = 1500; const ENOUGH_FRAMES: usize = 4; const MIN_FRAMES: usize = 2;
let mut max_syncs = MAX_SYNCS;
let mut first_frame: Option<MPEGFrame> = None;
let mut overall_sketchy = true;
let sync_positions = Self::iter_sync_async(reader, MAX_READ).await?;
for sync_offset in sync_positions {
if max_syncs == 0 {
break;
}
max_syncs -= 1;
reader.seek(SeekFrom::Start(sync_offset)).await?;
let mut frames = Vec::new();
for _ in 0..ENOUGH_FRAMES {
match Self::parse_frame_async(reader).await {
Ok(frame) => {
frames.push(frame);
if !frames
.last()
.expect("frames is non-empty after push")
.sketchy
{
break;
}
}
Err(_) => break,
}
}
if frames.len() >= MIN_FRAMES && first_frame.is_none() {
first_frame = Some(frames[0].clone());
}
if let Some(last_frame) = frames.last() {
if !last_frame.sketchy {
overall_sketchy = false;
return Ok((last_frame.clone(), overall_sketchy));
}
}
if frames.len() >= ENOUGH_FRAMES {
overall_sketchy = false;
return Ok((frames[0].clone(), overall_sketchy));
}
}
if let Some(frame) = first_frame {
Ok((frame, overall_sketchy))
} else {
Err(AudexError::InvalidData(
"can't sync to MPEG frame".to_string(),
))
}
}
async fn iter_sync_async(reader: &mut TokioFile, max_read: u64) -> Result<Vec<u64>> {
let mut positions = Vec::new();
let start_pos = reader.stream_position().await?;
let file_size = reader.seek(SeekFrom::End(0)).await?;
reader.seek(SeekFrom::Start(start_pos)).await?;
let remaining = file_size.saturating_sub(start_pos);
let read_size = max_read.min(remaining) as usize;
let mut buffer = vec![0u8; read_size];
let bytes_read = reader.read(&mut buffer).await?;
buffer.truncate(bytes_read);
const MAX_SYNC_POSITIONS: usize = 100_000;
for i in 0..buffer.len().saturating_sub(1) {
if buffer[i] == 0xFF && (buffer[i + 1] & 0xE0) == 0xE0 {
positions.push(start_pos + i as u64);
if positions.len() >= MAX_SYNC_POSITIONS {
break;
}
}
}
Ok(positions)
}
async fn parse_frame_async(reader: &mut TokioFile) -> Result<MPEGFrame> {
let offset = reader.stream_position().await?;
let mut buf = vec![0u8; 1024];
let bytes_read = reader.read(&mut buf).await?;
buf.truncate(bytes_read);
if bytes_read < 4 {
return Err(AudexError::InvalidData(
"Not enough data for MPEG frame".to_string(),
));
}
let mut cursor = std::io::Cursor::new(&buf[..]);
let mut frame = MPEGFrame::from_reader(&mut cursor)?;
frame.frame_offset = offset;
reader
.seek(SeekFrom::Start(offset + frame.frame_size as u64))
.await?;
Ok(frame)
}
async fn estimate_length_async(
info: &mut MPEGInfo,
reader: &mut TokioFile,
audio_start: u64,
) -> Result<()> {
let file_size = reader.seek(SeekFrom::End(0)).await?;
let content_size = file_size.saturating_sub(audio_start);
if info.bitrate > 0 && content_size > 0 {
let seconds = content_size as f64 * 8.0 / info.bitrate as f64;
info.length = Some(Duration::from_secs_f64(seconds));
}
Ok(())
}
pub async fn save_async(&mut self) -> Result<()> {
self.save_with_options_async(None, None, None, None).await
}
pub async fn save_with_options_async(
&mut self,
file_path: Option<&str>,
v1: Option<u8>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let target_path = match file_path {
Some(path) => path.to_string(),
None => self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
let v1_option = v1.unwrap_or(2);
let v2_version_option = v2_version.unwrap_or(3);
let v23_sep_string = v23_sep.map(|s| s.to_string());
if let Some(ref tags) = self.tags {
let config = crate::id3::tags::ID3SaveConfig {
v2_version: v2_version_option,
v2_minor: 0,
v23_sep: v23_sep_string.clone().unwrap_or_else(|| "/".to_string()),
v23_separator: v23_sep_string
.as_deref()
.and_then(|s| s.as_bytes().first().copied())
.unwrap_or(b'/'),
padding: None,
merge_frames: true,
preserve_unknown: true,
compress_frames: false,
write_v1: match v1_option {
0 => crate::id3::file::ID3v1SaveOptions::REMOVE,
1 => crate::id3::file::ID3v1SaveOptions::UPDATE,
_ => crate::id3::file::ID3v1SaveOptions::CREATE,
},
unsync: false,
extended_header: false,
convert_v24_frames: true,
};
tags.save_to_file_async(&target_path, &config).await?;
}
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
if let Some(ref mut tags) = self.tags {
tags.dict.clear();
tags.frames_by_id.clear();
}
self.save_async().await
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
crate::id3::file::clear_async(path.as_ref(), true, true).await
}
}
impl Default for MP3 {
fn default() -> Self {
Self::new()
}
}
impl FileType for MP3 {
type Tags = ID3Tags;
type Info = MPEGInfo;
fn format_id() -> &'static str {
"MP3"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::from_file(path)
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing MP3 file from reader");
let mut mp3 = Self::new();
match <ID3 as FileType>::load_from_reader(reader) {
Ok(id3) => {
debug_event!("ID3v2 tags parsed for MP3");
mp3.tags = Some(id3.tags);
}
Err(_) => {
mp3.tags = None;
}
}
reader.seek(std::io::SeekFrom::Start(0))?;
let mut reader = reader;
mp3.info = MPEGInfo::from_file(&mut reader)?;
debug_event!(
bitrate = mp3.info.bitrate,
sample_rate = mp3.info.sample_rate,
channels = mp3.info.channels,
"MPEG stream info parsed"
);
Ok(mp3)
}
fn save(&mut self) -> Result<()> {
MP3::save(self)
}
fn clear(&mut self) -> Result<()> {
if let Some(ref mut tags) = self.tags {
tags.dict.clear();
tags.frames_by_id.clear();
}
self.save()
}
fn save_to_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
if let Some(ref tags) = self.tags {
let mut id3_file = ID3::new();
id3_file.tags = tags.clone();
id3_file.save_to_writer(writer)
} else {
Err(AudexError::InvalidData("No tags to save".to_string()))
}
}
fn clear_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
crate::id3::file::clear_from_writer(writer, true, true)?;
self.tags = None;
Ok(())
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
self.save_with_options(path.to_str(), None, None, None)
}
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 add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
let mut tags = ID3Tags::new();
if let Some(ref filename) = self.filename {
tags.filename = Some(std::path::PathBuf::from(filename));
}
self.tags = Some(tags);
Ok(())
}
fn get(&self, key: &str) -> Option<Vec<String>> {
if key.eq_ignore_ascii_case("REPLAYGAIN_TRACK_GAIN") || key == "TXXX:REPLAYGAIN_TRACK_GAIN"
{
return self.get_rva2_replaygain("track", true);
}
if key.eq_ignore_ascii_case("REPLAYGAIN_TRACK_PEAK") || key == "TXXX:REPLAYGAIN_TRACK_PEAK"
{
return self.get_rva2_replaygain("track", false);
}
if key.eq_ignore_ascii_case("REPLAYGAIN_ALBUM_GAIN") || key == "TXXX:REPLAYGAIN_ALBUM_GAIN"
{
return self.get_rva2_replaygain("album", true);
}
if key.eq_ignore_ascii_case("REPLAYGAIN_ALBUM_PEAK") || key == "TXXX:REPLAYGAIN_ALBUM_PEAK"
{
return self.get_rva2_replaygain("album", false);
}
self.tags.as_ref()?.get_text_values(key)
}
fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self
.tags
.as_ref()
.map(|t| t.dict.keys().cloned().collect())
.unwrap_or_default();
let has_track_rva2 = keys
.iter()
.any(|k| k.starts_with("RVA2") && k.to_lowercase().contains("track"));
let has_album_rva2 = keys
.iter()
.any(|k| k.starts_with("RVA2") && k.to_lowercase().contains("album"));
if has_track_rva2 {
if !keys.iter().any(|k| k == "TXXX:REPLAYGAIN_TRACK_GAIN") {
keys.push("TXXX:REPLAYGAIN_TRACK_GAIN".to_string());
}
if !keys.iter().any(|k| k == "TXXX:REPLAYGAIN_TRACK_PEAK") {
keys.push("TXXX:REPLAYGAIN_TRACK_PEAK".to_string());
}
}
if has_album_rva2 {
if !keys.iter().any(|k| k == "TXXX:REPLAYGAIN_ALBUM_GAIN") {
keys.push("TXXX:REPLAYGAIN_ALBUM_GAIN".to_string());
}
if !keys.iter().any(|k| k == "TXXX:REPLAYGAIN_ALBUM_PEAK") {
keys.push("TXXX:REPLAYGAIN_ALBUM_PEAK".to_string());
}
}
keys
}
fn score(filename: &str, header: &[u8]) -> i32 {
let mut score = 0;
let filename_lower = filename.to_lowercase();
if header.len() >= 2 {
let _sync_word = (header[0] as u16) << 8 | header[1] as u16;
if header.starts_with(&[0xFF, 0xF2]) || header.starts_with(&[0xFF, 0xF3]) || header.starts_with(&[0xFF, 0xFA]) || header.starts_with(&[0xFF, 0xFB])
{
score += 2;
}
}
if header.len() >= 3 && header.starts_with(b"ID3") {
score += 2;
}
if filename_lower.ends_with(".mp3")
|| filename_lower.ends_with(".mp2")
|| filename_lower.ends_with(".mpg")
|| filename_lower.ends_with(".mpeg")
{
score += 1;
}
score
}
fn mime_types() -> &'static [&'static str] {
&["audio/mpeg", "audio/mp3", "audio/mpg", "audio/mpeg3"]
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MPEGInfo {
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::duration_as_secs_f64")
)]
pub length: Option<Duration>,
pub bitrate: u32,
pub sample_rate: u32,
pub channels: u16,
pub version: MPEGVersion,
pub layer: MPEGLayer,
pub channel_mode: ChannelMode,
pub emphasis: Emphasis,
pub protected: bool,
pub padding: bool,
pub private: bool,
pub copyright: bool,
pub original: bool,
pub mode_extension: u8,
pub sketchy: bool,
pub bitrate_mode: BitrateMode,
pub encoder_info: Option<String>,
pub encoder_settings: Option<String>,
pub track_gain: Option<f32>,
pub track_peak: Option<f32>,
pub album_gain: Option<f32>,
pub album_peak: Option<f32>,
}
impl MPEGInfo {
pub fn new() -> Self {
Self::default()
}
pub fn from_file<R: Read + Seek>(reader: &mut R) -> Result<Self> {
Self::skip_id3v2(reader)?;
let (frame, overall_sketchy) = Self::find_and_parse_frame(reader)?;
let mut info = MPEGInfo {
length: frame.length,
bitrate: frame.bitrate,
sample_rate: frame.sample_rate,
channels: frame.channels(),
version: frame.version,
layer: frame.layer,
channel_mode: frame.channel_mode,
emphasis: frame.emphasis,
protected: frame.protected,
padding: frame.padding,
private: frame.private,
copyright: frame.copyright,
original: frame.original,
mode_extension: frame.mode_extension,
sketchy: overall_sketchy, bitrate_mode: frame.bitrate_mode,
encoder_info: frame.encoder_info,
encoder_settings: frame.encoder_settings,
track_gain: frame.track_gain,
track_peak: frame.track_peak,
album_gain: frame.album_gain,
album_peak: None, };
if info.length.is_none() {
info.estimate_length_from_file_size(reader, frame.frame_offset)?;
}
Ok(info)
}
fn skip_id3v2<R: Read + Seek>(reader: &mut R) -> Result<()> {
reader.seek(SeekFrom::Start(0))?;
let file_size = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
const MAX_ID3_SKIP_ITERATIONS: usize = 1000;
let mut id3_iterations = 0usize;
loop {
id3_iterations += 1;
if id3_iterations > MAX_ID3_SKIP_ITERATIONS {
break;
}
let mut id3_header = [0u8; 10];
let mut bytes_read = 0usize;
while bytes_read < id3_header.len() {
let read_now = reader.read(&mut id3_header[bytes_read..])?;
if read_now == 0 {
break;
}
bytes_read += read_now;
}
if bytes_read < 10 {
reader.seek(SeekFrom::Start(0))?;
break;
}
if &id3_header[0..3] == b"ID3" {
let tag_size =
crate::id3::util::decode_synchsafe_int_checked(&id3_header[6..10])? as u64;
let current_pos = reader.stream_position()?;
if tag_size > 0 && current_pos + tag_size <= file_size {
let skip = i64::try_from(tag_size).map_err(|_| {
AudexError::InvalidData("ID3 tag size exceeds i64 range".to_string())
})?;
reader.seek(SeekFrom::Current(skip))?;
continue;
}
}
reader.seek(SeekFrom::Current(-(bytes_read as i64)))?;
break;
}
Ok(())
}
fn find_and_parse_frame<R: Read + Seek>(reader: &mut R) -> Result<(MPEGFrame, bool)> {
const MAX_READ: u64 = 1024 * 1024; const MAX_SYNCS: usize = 1500; const ENOUGH_FRAMES: usize = 4; const MIN_FRAMES: usize = 2;
let mut max_syncs = MAX_SYNCS;
let mut first_frame: Option<MPEGFrame> = None;
let mut overall_sketchy = true;
for sync_offset in crate::mp3::util::iter_sync(reader, MAX_READ)? {
if max_syncs == 0 {
break;
}
max_syncs -= 1;
reader.seek(SeekFrom::Start(sync_offset))?;
let mut frames = Vec::new();
for _ in 0..ENOUGH_FRAMES {
match MPEGFrame::from_reader(reader) {
Ok(frame) => {
frames.push(frame);
if !frames
.last()
.expect("frames is non-empty after push")
.sketchy
{
break;
}
}
Err(_) => break,
}
}
if frames.len() >= MIN_FRAMES && first_frame.is_none() {
first_frame = Some(frames[0].clone());
}
if let Some(last_frame) = frames.last() {
if !last_frame.sketchy {
overall_sketchy = false; return Ok((last_frame.clone(), overall_sketchy));
}
}
if frames.len() >= ENOUGH_FRAMES {
overall_sketchy = false; return Ok((frames[0].clone(), overall_sketchy));
}
}
if let Some(frame) = first_frame {
Ok((frame, overall_sketchy))
} else {
Err(AudexError::InvalidData(
"can't sync to MPEG frame".to_string(),
))
}
}
fn estimate_length_from_file_size<R: Read + Seek>(
&mut self,
reader: &mut R,
audio_start: u64,
) -> Result<()> {
let file_size = reader.seek(SeekFrom::End(0))?;
let content_size = file_size.saturating_sub(audio_start);
if self.bitrate > 0 && content_size > 0 {
let seconds = content_size as f64 * 8.0 / self.bitrate as f64;
self.length = Some(Duration::from_secs_f64(seconds));
}
Ok(())
}
}
impl Default for MPEGInfo {
fn default() -> Self {
Self {
length: None,
bitrate: 0,
sample_rate: 0,
channels: 0,
version: MPEGVersion::MPEG1,
layer: MPEGLayer::Layer3,
channel_mode: ChannelMode::Stereo,
emphasis: Emphasis::None,
protected: false,
padding: false,
private: false,
copyright: false,
original: false,
mode_extension: 0,
sketchy: false,
bitrate_mode: BitrateMode::Unknown,
encoder_info: None,
encoder_settings: None,
track_gain: None,
track_peak: None,
album_gain: None,
album_peak: None,
}
}
}
impl StreamInfo for MPEGInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
Some(self.bitrate) }
fn sample_rate(&self) -> Option<u32> {
Some(self.sample_rate)
}
fn channels(&self) -> Option<u16> {
Some(self.channels)
}
fn bits_per_sample(&self) -> Option<u16> {
None }
}
#[derive(Debug)]
pub struct EasyMP3 {
pub info: MPEGInfo,
pub tags: Option<crate::easyid3::EasyID3>,
pub filename: Option<String>,
}
impl EasyMP3 {
fn parse_language_code(lang: &str) -> Result<[u8; 3]> {
let bytes = lang.as_bytes();
if bytes.len() != 3 || !bytes.iter().all(|b| b.is_ascii_alphabetic()) {
return Err(AudexError::InvalidData(format!(
"Language code must be a 3-letter ASCII identifier, got '{}'",
lang
)));
}
Ok([
bytes[0].to_ascii_lowercase(),
bytes[1].to_ascii_lowercase(),
bytes[2].to_ascii_lowercase(),
])
}
fn ensure_easy_tags_mut(&mut self) -> Result<&mut crate::easyid3::EasyID3> {
if self.tags.is_none() {
self.add_tags()?;
}
self.tags
.as_mut()
.ok_or_else(|| AudexError::InvalidData("No tags available".to_string()))
}
fn load_easy_tags(path: &Path) -> Result<Option<crate::easyid3::EasyID3>> {
match crate::easyid3::EasyID3::load(path) {
Ok(tags) => Ok(Some(tags)),
Err(AudexError::ID3NoHeaderError) | Err(AudexError::HeaderNotFound) => Ok(None),
Err(AudexError::InvalidData(ref msg)) if msg.contains("No ID3 tags found") => Ok(None),
Err(err) => Err(err),
}
}
pub fn new() -> Self {
Self {
info: MPEGInfo::default(),
tags: None,
filename: None,
}
}
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
let easy_tags = Self::load_easy_tags(path)?;
let mp3 = MP3::from_file(path)?;
Ok(Self {
info: mp3.info,
tags: easy_tags,
filename: Some(path_str),
})
}
pub fn register_text_key(&mut self, _key: &str, _frame_id: &str) -> Result<()> {
self.ensure_easy_tags_mut()?
.register_text_key(_key, _frame_id)
}
pub fn register_txxx_key(&mut self, _key: &str, _description: &str) -> Result<()> {
self.ensure_easy_tags_mut()?
.register_txxx_key(_key, _description)
}
pub fn set_frame(&mut self, frame_id: &str, frame_data: Vec<String>) -> Result<()> {
self.ensure_easy_tags_mut()?
.id3
.add_text_frame(frame_id, frame_data)
}
pub fn set_tdat_frame(&mut self, date_ddmm: &str) -> Result<()> {
self.set_frame("TDAT", vec![date_ddmm.to_string()])
}
pub fn set_tpub_frame(&mut self, publisher: &str) -> Result<()> {
self.set_frame("TPUB", vec![publisher.to_string()])
}
pub fn set_txxx_frame(&mut self, description: &str, text: &str) -> Result<()> {
let frame_key = format!("TXXX:{}", description);
self.set_frame(&frame_key, vec![text.to_string()])
}
pub fn set_comm_frame(&mut self, text: &str, _lang: &str) -> Result<()> {
use crate::id3::{COMM, specs::TextEncoding};
let lang = Self::parse_language_code(_lang)?;
let frame = COMM::new(TextEncoding::Utf16, lang, String::new(), text.to_string());
self.ensure_easy_tags_mut()?.id3.add(Box::new(frame))
}
pub fn set_uslt_frame(&mut self, lyrics: &str, _lang: &str) -> Result<()> {
use crate::id3::{USLT, specs::TextEncoding};
let lang = Self::parse_language_code(_lang)?;
let frame = USLT::new(TextEncoding::Utf16, lang, String::new(), lyrics.to_string());
self.ensure_easy_tags_mut()?.id3.add(Box::new(frame))
}
pub fn set_apic_frame(
&mut self,
_data: &[u8],
_mime: &str,
_pic_type: u8,
_description: &str,
) -> Result<()> {
use crate::id3::{APIC, PictureType, specs::TextEncoding};
let frame = APIC::new(
TextEncoding::Utf16,
_mime.to_string(),
PictureType::from(_pic_type),
_description.to_string(),
_data.to_vec(),
);
self.ensure_easy_tags_mut()?.id3.add(Box::new(frame))
}
pub fn save(&mut self) -> Result<()> {
debug_event!("saving EasyMP3 tags");
self.save_with_options(None, None, None, None)
}
pub fn save_with_options(
&mut self,
file_path: Option<&str>,
v1: Option<u8>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let target_path = match file_path {
Some(path) => path,
None => self.filename.as_deref().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
let v1_option = v1.unwrap_or(2); let v2_version_option = v2_version.unwrap_or(3); let v23_sep_string = v23_sep.map(|s| s.to_string());
let tags = self
.tags
.as_mut()
.ok_or_else(|| AudexError::InvalidData("No tags available for saving".to_string()))?;
tags.id3.save(
target_path,
v1_option,
v2_version_option,
v23_sep_string,
None,
)?;
Ok(())
}
}
impl Default for EasyMP3 {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "async")]
impl EasyMP3 {
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::from_file_async(path).await
}
pub async fn from_file_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let path = path.as_ref();
let path_str = path.to_string_lossy().to_string();
let easy_tags = match crate::easyid3::EasyID3::load_async(path).await {
Ok(tags) => Some(tags),
Err(AudexError::ID3NoHeaderError) | Err(AudexError::HeaderNotFound) => None,
Err(AudexError::InvalidData(ref msg)) if msg.contains("No ID3 tags found") => None,
Err(err) => return Err(err),
};
let mp3 = MP3::from_file_async(path).await?;
Ok(Self {
info: mp3.info,
tags: easy_tags,
filename: Some(path_str),
})
}
pub async fn save_async(&mut self) -> Result<()> {
self.save_with_options_async(None, None, None, None).await
}
pub async fn save_with_options_async(
&mut self,
file_path: Option<&str>,
v1: Option<u8>,
v2_version: Option<u8>,
v23_sep: Option<&str>,
) -> Result<()> {
let target_path = match file_path {
Some(path) => path.to_string(),
None => self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No file path provided and no filename stored".to_string())
})?,
};
let v1_option = v1.unwrap_or(2);
let v2_version_option = v2_version.unwrap_or(3);
let v23_sep_string = v23_sep.map(|s| s.to_string());
let tags = self
.tags
.as_ref()
.ok_or_else(|| AudexError::InvalidData("No tags available for saving".to_string()))?;
let config = crate::id3::tags::ID3SaveConfig {
v2_version: v2_version_option,
v2_minor: 0,
v23_sep: v23_sep_string.clone().unwrap_or_else(|| "/".to_string()),
v23_separator: v23_sep_string
.as_deref()
.and_then(|s| s.as_bytes().first().copied())
.unwrap_or(b'/'),
padding: None,
merge_frames: true,
preserve_unknown: true,
compress_frames: false,
write_v1: match v1_option {
0 => crate::id3::file::ID3v1SaveOptions::REMOVE,
1 => crate::id3::file::ID3v1SaveOptions::UPDATE,
_ => crate::id3::file::ID3v1SaveOptions::CREATE,
},
unsync: false,
extended_header: false,
convert_v24_frames: true,
};
tags.id3.save_to_file_async(&target_path, &config).await?;
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
if let Some(ref mut tags) = self.tags {
tags.id3.dict.clear();
tags.id3.frames_by_id.clear();
}
self.save_async().await
}
}
impl FileType for EasyMP3 {
type Tags = crate::easyid3::EasyID3;
type Info = MPEGInfo;
fn format_id() -> &'static str {
"EasyMP3"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::from_file(path)
}
fn save(&mut self) -> Result<()> {
EasyMP3::save(self)
}
fn clear(&mut self) -> Result<()> {
if let Some(ref mut tags) = self.tags {
tags.clear()?;
}
self.save()
}
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 add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
let mut tags = crate::easyid3::EasyID3::new();
if let Some(ref filename) = self.filename {
tags.filename = Some(filename.clone());
}
self.tags = Some(tags);
Ok(())
}
fn score(filename: &str, header: &[u8]) -> i32 {
MP3::score(filename, header)
}
fn mime_types() -> &'static [&'static str] {
MP3::mime_types()
}
}