use crate::VERSION_STRING;
use crate::ogg::OggPage;
use crate::vorbis::VCommentDict;
use crate::{AudexError, FileType, Result, StreamInfo};
use byteorder::{LittleEndian, ReadBytesExt};
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::time::Duration;
const MAX_PAGE_SEARCH: usize = 1024;
#[cfg(feature = "async")]
use tokio::fs::{File as TokioFile, OpenOptions as TokioOpenOptions};
#[cfg(feature = "async")]
use tokio::io::{AsyncSeekExt, BufReader as TokioBufReader};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OggVorbisError {
#[error("Ogg Vorbis error: {0}")]
General(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid data: {0}")]
InvalidData(String),
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum OggVorbisHeaderError {
#[error("Header error: {0}")]
InvalidHeader(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
impl From<OggVorbisError> for AudexError {
fn from(err: OggVorbisError) -> Self {
match err {
OggVorbisError::General(msg) => AudexError::InvalidData(msg),
OggVorbisError::Io(e) => AudexError::Io(e),
OggVorbisError::InvalidData(msg) => AudexError::InvalidData(msg),
}
}
}
impl From<OggVorbisHeaderError> for AudexError {
fn from(err: OggVorbisHeaderError) -> Self {
match err {
OggVorbisHeaderError::InvalidHeader(msg) => AudexError::InvalidData(msg),
OggVorbisHeaderError::Io(e) => AudexError::Io(e),
}
}
}
#[derive(Debug, Clone)]
pub struct OggVCommentDict {
inner: VCommentDict,
}
impl Default for OggVCommentDict {
fn default() -> Self {
Self::new()
}
}
impl OggVCommentDict {
pub fn new() -> Self {
Self {
inner: VCommentDict::new(),
}
}
pub fn from_fileobj<R: Read + Seek>(fileobj: &mut R, info: &OggVorbisInfo) -> Result<Self> {
let mut pages = Vec::new();
let mut complete = false;
let mut pages_read = 0usize;
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
while !complete {
let page = OggPage::from_reader(fileobj)?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for comment packet".to_string(),
));
}
if page.serial == info.serial {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"OGG Vorbis comment packet",
)?;
pages.push(page.clone());
complete = page.is_complete() || page.packets.len() > 1;
}
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 7 {
return Err(AudexError::InvalidData(
"Invalid Vorbis comment packet".to_string(),
));
}
if &packets[0][..7] != b"\x03vorbis" {
return Err(AudexError::InvalidData(format!(
"Expected Vorbis comment header (\\x03vorbis), got {:?}",
&packets[0][..7]
)));
}
let data = &packets[0][7..];
let inner =
VCommentDict::from_bytes_with_options(data, crate::vorbis::ErrorMode::Replace, true)?;
let _original_data = Some(data.to_vec());
let mut size_buffer = Vec::new();
inner.write(&mut size_buffer, Some(true))?;
let _vcomment_size = size_buffer.len();
let _original_padding = data.len().saturating_sub(_vcomment_size);
Ok(Self { inner })
}
pub fn inject<R: Read + Write + Seek + 'static>(
&self,
fileobj: &mut R,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
fileobj.seek(SeekFrom::Start(0))?;
let first_page = OggPage::from_reader(fileobj)?;
let stream_serial = first_page.serial;
let mut page = first_page;
let mut pages_read = 1usize;
while page.packets.is_empty()
|| !page.packets[0].starts_with(b"\x03vorbis")
|| page.serial != stream_serial
{
page = OggPage::from_reader(fileobj)?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for comment header".to_string(),
));
}
}
let mut old_pages = vec![page];
loop {
let last_page = old_pages.last().ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list while reading comments".into(),
)
})?;
if last_page.is_complete() || last_page.packets.len() > 1 {
break;
}
let page = OggPage::from_reader(fileobj)?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while reading comment pages".to_string(),
));
}
if page.serial == old_pages[0].serial {
old_pages.push(page);
}
}
let packets = OggPage::to_packets(&old_pages, false)?;
if packets.is_empty() {
return Err(AudexError::InvalidData("No packets found".to_string()));
}
let content_size = {
let old_pos = fileobj.stream_position()?;
let file_size = fileobj.seek(SeekFrom::End(0))?;
fileobj.seek(SeekFrom::Start(old_pos))?; i64::try_from(file_size)
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(packets[0].len()).unwrap_or(0))
};
let vcomment_data = {
let mut data = b"\x03vorbis".to_vec();
let mut vcomment_bytes = Vec::new();
let mut comment_to_write = self.inner.clone();
if !comment_to_write.keys().is_empty() {
comment_to_write.set_vendor(format!("Audex {}", VERSION_STRING));
}
comment_to_write.write(&mut vcomment_bytes, Some(true))?;
data.extend_from_slice(&vcomment_bytes);
data
};
let padding_left = packets[0].len() as i64 - vcomment_data.len() as i64;
let info = crate::tags::PaddingInfo::new(padding_left, content_size);
let new_padding = info.get_padding_with(padding_func);
let mut new_packets = packets;
new_packets[0] = vcomment_data;
let padding_bytes = if new_padding < 0 {
0usize
} else {
usize::try_from(new_padding).unwrap_or(0)
};
if padding_bytes > 0 {
new_packets[0].extend_from_slice(&vec![0u8; padding_bytes]);
}
let new_pages = OggPage::from_packets_try_preserve(new_packets.clone(), &old_pages);
let final_pages = if new_pages.is_empty() {
let first_sequence = old_pages[0].sequence;
let raw_position = old_pages
.last()
.ok_or_else(|| AudexError::InvalidData("no comment pages found".to_string()))?
.position;
let original_granule = if raw_position < 0 {
0u64
} else {
raw_position as u64
};
OggPage::from_packets_with_options(
new_packets,
first_sequence,
4096,
2048,
original_granule,
)?
} else {
new_pages
};
if final_pages.is_empty() {
return Err(AudexError::InvalidData(
"Failed to create new OGG pages".to_string(),
));
}
OggPage::replace(fileobj, &old_pages, final_pages)?;
Ok(())
}
}
impl std::ops::Deref for OggVCommentDict {
type Target = VCommentDict;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for OggVCommentDict {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
use crate::Tags;
impl Tags for OggVCommentDict {
fn get(&self, key: &str) -> Option<&[String]> {
self.inner.get(key)
}
fn set(&mut self, key: &str, values: Vec<String>) {
self.inner.set(key, values)
}
fn remove(&mut self, key: &str) {
self.inner.remove(key)
}
fn keys(&self) -> Vec<String> {
self.inner.keys()
}
fn pprint(&self) -> String {
self.inner.pprint()
}
}
#[derive(Debug, Default)]
pub struct OggVorbis {
pub info: Option<OggVorbisInfo>,
pub tags: Option<OggVCommentDict>,
filename: Option<PathBuf>,
}
#[derive(Debug, Clone, Default)]
pub struct OggVorbisInfo {
pub length: Option<Duration>,
pub bitrate: Option<u32>,
pub sample_rate: u32,
pub channels: u16,
pub serial: u32,
pub version: u32,
pub max_bitrate: Option<u32>,
pub nominal_bitrate: Option<u32>,
pub min_bitrate: Option<u32>,
}
impl StreamInfo for OggVorbisInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
}
fn sample_rate(&self) -> Option<u32> {
if self.sample_rate > 0 {
Some(self.sample_rate)
} else {
None
}
}
fn channels(&self) -> Option<u16> {
if self.channels > 0 {
Some(self.channels)
} else {
None
}
}
fn bits_per_sample(&self) -> Option<u16> {
None }
}
impl OggVorbisInfo {
pub fn from_identification_header(packet: &[u8]) -> Result<Self> {
if packet.len() < 30 {
return Err(AudexError::InvalidData(
"Vorbis identification header too short".to_string(),
));
}
if packet[0] != 1 || &packet[1..7] != b"vorbis" {
return Err(AudexError::InvalidData(
"Invalid Vorbis identification header".to_string(),
));
}
let mut cursor = Cursor::new(&packet[7..]);
let version = cursor.read_u32::<LittleEndian>()?;
let channels = cursor.read_u8()? as u16;
let sample_rate = cursor.read_u32::<LittleEndian>()?;
let max_bitrate = cursor.read_u32::<LittleEndian>()?;
let nominal_bitrate = cursor.read_u32::<LittleEndian>()?;
let min_bitrate = cursor.read_u32::<LittleEndian>()?;
if sample_rate == 0 {
return Err(OggVorbisHeaderError::InvalidHeader(
"sample rate can't be zero".to_string(),
)
.into());
}
if channels == 0 {
return Err(AudexError::InvalidData(
"Channel count cannot be zero".to_string(),
));
}
let max_bitrate = if max_bitrate == 0xFFFFFFFF {
0
} else {
max_bitrate
};
let nominal_bitrate = if nominal_bitrate == 0xFFFFFFFF {
0
} else {
nominal_bitrate
};
let min_bitrate = if min_bitrate == 0xFFFFFFFF {
0
} else {
min_bitrate
};
let bitrate = if nominal_bitrate == 0 {
let avg = ((max_bitrate as u64 + min_bitrate as u64) / 2) as u32;
if avg > 0 { Some(avg) } else { None }
} else if max_bitrate > 0 && max_bitrate < nominal_bitrate {
Some(max_bitrate)
} else if min_bitrate > nominal_bitrate {
Some(min_bitrate)
} else {
Some(nominal_bitrate)
};
let max_bitrate_opt = if max_bitrate > 0 {
Some(max_bitrate)
} else {
None
};
let nominal_bitrate_opt = if nominal_bitrate > 0 {
Some(nominal_bitrate)
} else {
None
};
let min_bitrate_opt = if min_bitrate > 0 {
Some(min_bitrate)
} else {
None
};
Ok(Self {
length: None, bitrate,
sample_rate,
channels,
serial: 0, version,
max_bitrate: max_bitrate_opt,
nominal_bitrate: nominal_bitrate_opt,
min_bitrate: min_bitrate_opt,
})
}
pub fn set_length(&mut self, position: i64) {
if self.sample_rate > 0 && position > 0 {
let duration_secs = position as f64 / self.sample_rate as f64;
if duration_secs.is_finite() && duration_secs <= u64::MAX as f64 {
self.length = Some(Duration::from_secs_f64(duration_secs));
}
} else {
self.length = None;
}
}
pub fn pprint(&self) -> String {
let duration = self
.length
.map(|d| format!("{:.2}", d.as_secs_f64()))
.unwrap_or_else(|| "0.00".to_string());
let bitrate = self.bitrate.unwrap_or(0);
format!("Ogg Vorbis, {} seconds, {} bps", duration, bitrate)
}
pub fn post_tags<R: Read + Seek>(&mut self, fileobj: &mut R) -> Result<()> {
let last_page = OggPage::find_last(fileobj, self.serial, true)?
.ok_or_else(|| AudexError::InvalidData("could not find last page".to_string()))?;
if last_page.position > 0 {
let length_secs = last_page.position as f64 / self.sample_rate as f64;
if length_secs.is_finite() && length_secs >= 0.0 && length_secs <= u64::MAX as f64 {
self.length = Some(Duration::from_secs_f64(length_secs));
}
}
Ok(())
}
}
impl FileType for OggVorbis {
type Tags = OggVCommentDict;
type Info = OggVorbisInfo;
fn format_id() -> &'static str {
"OggVorbis"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
use std::fs::File;
use std::io::BufReader;
debug_event!("parsing OGG Vorbis file");
let path_buf = path.as_ref().to_path_buf();
let file = File::open(&path_buf)?;
let mut reader = BufReader::new(file);
reader.seek(std::io::SeekFrom::Start(0))?;
let mut page = OggPage::from_reader(&mut reader)?;
if page.packets.is_empty() {
return Err(
OggVorbisHeaderError::InvalidHeader("page has not packets".to_string()).into(),
);
}
let mut pages_read = 1usize;
while page.packets.is_empty() || !page.packets[0].starts_with(b"\x01vorbis") {
page = OggPage::from_reader(&mut reader)?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for identification header".to_string(),
));
}
}
if !page.is_first() {
return Err(OggVorbisHeaderError::InvalidHeader(
"page has ID header, but doesn't start a stream".to_string(),
)
.into());
}
if page.packets[0].len() < 28 {
return Err(OggVorbisHeaderError::InvalidHeader(
"page contains a packet too short to be valid".to_string(),
)
.into());
}
let mut info = OggVorbisInfo::from_identification_header(&page.packets[0])?;
info.serial = page.serial;
let tags = OggVCommentDict::from_fileobj(&mut reader, &info)?;
debug_event!(tag_count = tags.keys().len(), "OGG Vorbis tags loaded");
info.post_tags(&mut reader)?;
Ok(Self {
info: Some(info),
tags: Some(tags),
filename: Some(path_buf),
})
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing OGG Vorbis file from reader");
let mut reader = reader;
reader.seek(std::io::SeekFrom::Start(0))?;
let mut page = OggPage::from_reader(&mut reader)?;
if page.packets.is_empty() {
return Err(
OggVorbisHeaderError::InvalidHeader("page has not packets".to_string()).into(),
);
}
let mut pages_read = 1usize;
while page.packets.is_empty() || !page.packets[0].starts_with(b"\x01vorbis") {
page = OggPage::from_reader(&mut reader)?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for identification header".to_string(),
));
}
}
if !page.is_first() {
return Err(OggVorbisHeaderError::InvalidHeader(
"page has ID header, but doesn't start a stream".to_string(),
)
.into());
}
if page.packets[0].len() < 28 {
return Err(OggVorbisHeaderError::InvalidHeader(
"page contains a packet too short to be valid".to_string(),
)
.into());
}
let mut info = OggVorbisInfo::from_identification_header(&page.packets[0])?;
info.serial = page.serial;
let tags = OggVCommentDict::from_fileobj(&mut reader, &info)?;
info.post_tags(&mut reader)?;
Ok(Self {
info: Some(info),
tags: Some(tags),
filename: None,
})
}
fn save(&mut self) -> Result<()> {
debug_event!("saving OGG Vorbis metadata");
if let Some(path) = self.filename.clone() {
self.save_with_options(Some(path), None)
} else {
warn_event!("no filename available for OGG Vorbis save");
Err(AudexError::InvalidData(
"No filename available for saving".to_string(),
))
}
}
fn clear(&mut self) -> Result<()> {
let prev_tags = self.tags.take();
let mut empty = OggVCommentDict::new();
empty.set_vendor(String::new());
self.tags = Some(empty);
if let Err(e) = self.save() {
self.tags = prev_tags;
return Err(e);
}
Ok(())
}
fn save_to_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
let tags = self
.tags
.as_ref()
.ok_or_else(|| AudexError::InvalidData("No tags available for saving".to_string()))?;
let buf = crate::util::read_all_from_writer_limited(writer, "in-memory Ogg Vorbis save")?;
let mut cursor = Cursor::new(buf);
tags.inject(&mut cursor, None)?;
let result = cursor.into_inner();
writer.seek(std::io::SeekFrom::Start(0))?;
writer.write_all(&result)?;
crate::util::truncate_writer_dyn(writer, result.len() as u64)?;
Ok(())
}
fn clear_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
let mut empty = OggVCommentDict::new();
empty.set_vendor(String::new());
self.tags = Some(empty);
self.save_to_writer(writer)
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
self.save_with_options(Some(path), None)
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(OggVCommentDict::new());
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 {
static DEFAULT_INFO: OggVorbisInfo = OggVorbisInfo {
length: None,
bitrate: None,
sample_rate: 0,
channels: 0,
serial: 0,
version: 0,
max_bitrate: None,
nominal_bitrate: None,
min_bitrate: None,
};
self.info.as_ref().unwrap_or(&DEFAULT_INFO)
}
fn score(_filename: &str, header: &[u8]) -> i32 {
let has_ogg_signature = header.len() >= 4 && &header[0..4] == b"OggS";
let has_vorbis_marker =
header.len() >= 7 && header.windows(7).any(|window| window == b"\x01vorbis");
if has_ogg_signature && has_vorbis_marker {
1
} else {
0
}
}
fn mime_types() -> &'static [&'static str] {
&[
"audio/ogg",
"audio/vorbis",
"audio/x-vorbis",
"application/ogg",
]
}
}
impl OggVorbis {
pub fn new<P: AsRef<Path>>(path: P) -> Result<Self> {
Self::load(path)
}
pub fn save_with_options<P>(
&mut self,
path: Option<P>,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()>
where
P: AsRef<Path>,
{
use std::fs::OpenOptions;
let (file_path, is_new_path) = match &path {
Some(p) => (p.as_ref().to_path_buf(), true),
None => (
self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No filename available for saving".to_string())
})?,
false,
),
};
let tags = self
.tags
.as_ref()
.ok_or_else(|| AudexError::InvalidData("No tags available for saving".to_string()))?;
let mut file = OpenOptions::new().read(true).write(true).open(&file_path)?;
tags.inject(&mut file, padding_func)?;
if is_new_path {
self.filename = Some(file_path);
}
Ok(())
}
pub const ERROR: &'static str = "OggVorbisHeaderError";
pub const MIMES: &'static [&'static str] = &["audio/vorbis", "audio/x-vorbis"];
pub fn score_static(_filename: &str, _fileobj: &mut dyn Read, header: &[u8]) -> i32 {
let has_ogg_signature = header.len() >= 4 && &header[0..4] == b"OggS";
let has_vorbis_marker =
header.len() >= 7 && header.windows(7).any(|window| window == b"\x01vorbis");
if has_ogg_signature && has_vorbis_marker {
1
} else {
0
}
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(OggVCommentDict::new());
Ok(())
}
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
let mut vorbis = OggVorbis::load(path)?;
vorbis.clear()
}
impl OggVCommentDict {
pub fn from_inner(inner: VCommentDict) -> Self {
let mut result = Self::new();
for key in inner.keys() {
if let Some(values) = inner.get(&key) {
result.set(&key, values.to_vec());
}
}
result.set_vendor(inner.vendor().to_string());
result
}
}
#[cfg(feature = "async")]
impl OggVorbis {
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let path_buf = path.as_ref().to_path_buf();
let file = TokioFile::open(&path_buf).await?;
let mut reader = TokioBufReader::new(file);
reader.seek(SeekFrom::Start(0)).await?;
let mut page = OggPage::from_reader_async(&mut reader).await?;
if page.packets.is_empty() {
return Err(
OggVorbisHeaderError::InvalidHeader("page has no packets".to_string()).into(),
);
}
let mut pages_read = 1usize;
while page.packets.is_empty() || !page.packets[0].starts_with(b"\x01vorbis") {
page = OggPage::from_reader_async(&mut reader).await?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for identification header".to_string(),
));
}
}
if !page.is_first() {
return Err(OggVorbisHeaderError::InvalidHeader(
"page has ID header, but doesn't start a stream".to_string(),
)
.into());
}
if page.packets[0].len() < 28 {
return Err(OggVorbisHeaderError::InvalidHeader(
"page contains a packet too short to be valid".to_string(),
)
.into());
}
let mut info = OggVorbisInfo::from_identification_header(&page.packets[0])?;
info.serial = page.serial;
let tags = Self::parse_tags_async(&mut reader, &info).await?;
Self::post_tags_async(&mut reader, &mut info).await?;
Ok(Self {
info: Some(info),
tags: Some(tags),
filename: Some(path_buf),
})
}
async fn parse_tags_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
info: &OggVorbisInfo,
) -> Result<OggVCommentDict> {
let mut pages = Vec::new();
let mut complete = false;
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
let mut pages_read = 0usize;
while !complete {
let page = OggPage::from_reader_async(reader).await?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for comment packet".to_string(),
));
}
if page.serial == info.serial {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"OGG Vorbis comment packet",
)?;
pages.push(page.clone());
complete = page.is_complete() || page.packets.len() > 1;
}
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 7 {
return Err(AudexError::InvalidData(
"Invalid Vorbis comment packet".to_string(),
));
}
let data = &packets[0][7..];
let inner =
VCommentDict::from_bytes_with_options(data, crate::vorbis::ErrorMode::Replace, true)?;
Ok(OggVCommentDict::from_inner(inner))
}
async fn post_tags_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
info: &mut OggVorbisInfo,
) -> Result<()> {
let last_page = OggPage::find_last_async(reader, info.serial, true)
.await?
.ok_or_else(|| AudexError::InvalidData("could not find last page".to_string()))?;
if last_page.position > 0 {
let length_secs = last_page.position as f64 / info.sample_rate as f64;
if length_secs.is_finite() && length_secs >= 0.0 && length_secs <= u64::MAX as f64 {
info.length = Some(Duration::from_secs_f64(length_secs));
}
}
Ok(())
}
pub async fn save_async(&mut self) -> Result<()> {
if let Some(path) = self.filename.clone() {
self.save_with_options_async(Some(path), None).await
} else {
Err(AudexError::InvalidData(
"No filename available for saving".to_string(),
))
}
}
pub async fn save_with_options_async<P>(
&mut self,
path: Option<P>,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()>
where
P: AsRef<Path>,
{
let (file_path, is_new_path) = match &path {
Some(p) => (p.as_ref().to_path_buf(), true),
None => (
self.filename.clone().ok_or_else(|| {
AudexError::InvalidData("No filename available for saving".to_string())
})?,
false,
),
};
let tags = self
.tags
.as_ref()
.ok_or_else(|| AudexError::InvalidData("No tags available for saving".to_string()))?;
let mut file = TokioOpenOptions::new()
.read(true)
.write(true)
.open(&file_path)
.await?;
Self::inject_tags_async(&mut file, tags, padding_func).await?;
if is_new_path {
self.filename = Some(file_path);
}
Ok(())
}
async fn inject_tags_async(
fileobj: &mut TokioFile,
tags: &OggVCommentDict,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
fileobj.seek(SeekFrom::Start(0)).await?;
let mut page = OggPage::from_reader_async(fileobj).await?;
let mut pages_read = 1usize;
while page.packets.is_empty() || !page.packets[0].starts_with(b"\x03vorbis") {
page = OggPage::from_reader_async(fileobj).await?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for comment header".to_string(),
));
}
}
let mut old_pages = vec![page];
loop {
let last_page = old_pages.last().ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list while reading comments".into(),
)
})?;
if last_page.is_complete() || last_page.packets.len() > 1 {
break;
}
let page = OggPage::from_reader_async(fileobj).await?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while reading comment pages".to_string(),
));
}
if page.serial == old_pages[0].serial {
old_pages.push(page);
}
}
let packets = OggPage::to_packets(&old_pages, false)?;
if packets.is_empty() {
return Err(AudexError::InvalidData("No packets found".to_string()));
}
let content_size = {
let old_pos = fileobj.stream_position().await?;
let file_size = fileobj.seek(SeekFrom::End(0)).await?;
fileobj.seek(SeekFrom::Start(old_pos)).await?;
i64::try_from(file_size)
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(packets[0].len()).unwrap_or(0))
};
let vcomment_data = {
let mut data = b"\x03vorbis".to_vec();
let mut vcomment_bytes = Vec::new();
let mut comment_to_write = VCommentDict::clone(&**tags);
if !comment_to_write.keys().is_empty() {
comment_to_write.set_vendor(format!("Audex {}", VERSION_STRING));
}
comment_to_write.write(&mut vcomment_bytes, Some(true))?;
data.extend_from_slice(&vcomment_bytes);
data
};
let padding_left = packets[0].len() as i64 - vcomment_data.len() as i64;
let info = crate::tags::PaddingInfo::new(padding_left, content_size);
let new_padding = info.get_padding_with(padding_func);
let mut new_packets = packets;
new_packets[0] = vcomment_data;
let padding_bytes = if new_padding < 0 {
0usize
} else {
usize::try_from(new_padding).unwrap_or(0)
};
if padding_bytes > 0 {
new_packets[0].extend_from_slice(&vec![0u8; padding_bytes]);
}
let new_pages = OggPage::from_packets_try_preserve(new_packets.clone(), &old_pages);
let final_pages = if new_pages.is_empty() {
let first_sequence = old_pages[0].sequence;
let raw_position = old_pages
.last()
.ok_or_else(|| AudexError::InvalidData("no comment pages found".to_string()))?
.position;
let original_granule = if raw_position < 0 {
0u64
} else {
raw_position as u64
};
OggPage::from_packets_with_options(
new_packets,
first_sequence,
4096,
2048,
original_granule,
)?
} else {
new_pages
};
if final_pages.is_empty() {
return Err(AudexError::InvalidData(
"Failed to create new OGG pages".to_string(),
));
}
OggPage::replace_async(fileobj, &old_pages, final_pages).await?;
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
let mut empty = OggVCommentDict::new();
empty.set_vendor(String::new());
self.tags = Some(empty);
self.save_async().await
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
let mut vorbis = Self::load_async(path).await?;
vorbis.clear_async().await
}
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
OggVorbis::delete_async(path).await
}