use crate::VERSION_STRING;
use crate::ogg::OggPage;
use crate::vorbis::VCommentDict;
use crate::{AudexError, FileType, Result, StreamInfo};
use byteorder::{LittleEndian, ReadBytesExt};
use std::fs::File;
use std::io::{BufReader, Cursor, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "async")]
use tokio::fs::{File as TokioFile, OpenOptions as TokioOpenOptions};
#[cfg(feature = "async")]
use tokio::io::{AsyncSeekExt, BufReader as TokioBufReader};
#[derive(Debug, Clone, PartialEq)]
pub struct SpeexError(pub String);
impl std::fmt::Display for SpeexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for SpeexError {}
#[derive(Debug, Clone, PartialEq)]
pub struct SpeexHeaderError(pub SpeexError);
impl std::fmt::Display for SpeexHeaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Speex header error: {}", self.0)
}
}
impl std::error::Error for SpeexHeaderError {}
impl From<SpeexError> for SpeexHeaderError {
fn from(err: SpeexError) -> Self {
SpeexHeaderError(err)
}
}
impl From<SpeexHeaderError> for AudexError {
fn from(err: SpeexHeaderError) -> Self {
AudexError::InvalidData(err.to_string())
}
}
impl From<SpeexError> for AudexError {
fn from(err: SpeexError) -> Self {
AudexError::InvalidData(err.0)
}
}
#[derive(Debug, Clone, Default)]
pub struct SpeexInfo {
pub length: Option<Duration>,
pub channels: u16,
pub sample_rate: u32,
pub bitrate: Option<i32>,
pub serial: u32,
pub version: String,
pub version_id: u32,
pub mode: i32,
pub mode_bitstream_version: i32,
pub nb_channels: u32,
pub nb_frames: i32,
pub frame_size: u32,
pub vbr: i32,
pub frames_per_packet: u32,
pub extra_headers: u32,
pub reserved1: u32,
pub reserved2: u32,
}
impl SpeexInfo {
pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self> {
let mut speex_info = Self::default();
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(SpeexHeaderError(SpeexError(
"No Speex stream found within page limit".to_string(),
))
.into());
}
let page = match OggPage::from_reader(reader) {
Ok(page) => page,
Err(_) => {
return Err(
SpeexHeaderError(SpeexError("No Speex stream found".to_string())).into(),
);
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && &first_packet[0..8] == b"Speex " {
speex_info.serial = page.serial;
speex_info.parse_identification_packet(first_packet)?;
speex_info.post_tags(reader)?;
return Ok(speex_info);
}
}
}
}
fn parse_identification_packet(&mut self, packet: &[u8]) -> Result<()> {
if packet.len() < 80 {
return Err(SpeexHeaderError(SpeexError(
"Invalid Speex identification packet length".to_string(),
))
.into());
}
let mut cursor = Cursor::new(&packet[8..]);
let mut version_bytes = [0u8; 20];
cursor.read_exact(&mut version_bytes)?;
let version_len = version_bytes.iter().position(|&x| x == 0).unwrap_or(20);
self.version = String::from_utf8_lossy(&version_bytes[..version_len]).into_owned();
self.version_id = cursor.read_u32::<LittleEndian>()?;
let _header_size = cursor.read_u32::<LittleEndian>()?;
self.sample_rate = cursor.read_u32::<LittleEndian>()?;
self.mode = cursor.read_i32::<LittleEndian>()?;
self.mode_bitstream_version = cursor.read_i32::<LittleEndian>()?;
self.nb_channels = cursor.read_u32::<LittleEndian>()?;
if self.nb_channels > u16::MAX as u32 {
return Err(SpeexHeaderError(SpeexError(format!(
"Channel count {} exceeds maximum supported value of {}",
self.nb_channels,
u16::MAX
)))
.into());
}
self.channels = self.nb_channels as u16;
let raw_bitrate = cursor.read_i32::<LittleEndian>()?;
self.bitrate = if raw_bitrate >= 0 {
Some(raw_bitrate)
} else {
None
};
self.frame_size = cursor.read_u32::<LittleEndian>()?;
self.vbr = cursor.read_i32::<LittleEndian>()?;
self.frames_per_packet = cursor.read_u32::<LittleEndian>()?;
self.extra_headers = cursor.read_u32::<LittleEndian>()?;
self.reserved1 = cursor.read_u32::<LittleEndian>()?;
self.reserved2 = cursor.read_u32::<LittleEndian>()?;
if self.sample_rate == 0 {
return Err(
SpeexHeaderError(SpeexError("Sample rate cannot be zero".to_string())).into(),
);
}
if self.nb_channels == 0 {
return Err(
SpeexHeaderError(SpeexError("Channel count cannot be zero".to_string())).into(),
);
}
Ok(())
}
fn post_tags<R: Read + Seek>(&mut self, reader: &mut R) -> Result<()> {
let last_page = match OggPage::find_last_with_finishing(reader, self.serial, true)? {
Some(page) => page,
None => return Ok(()),
};
if last_page.position >= 0 && self.sample_rate > 0 {
let duration_secs = last_page.position as f64 / self.sample_rate as f64;
if duration_secs.is_finite() && duration_secs >= 0.0 && duration_secs <= u64::MAX as f64
{
self.length = Some(Duration::from_secs_f64(duration_secs));
}
}
Ok(())
}
pub fn pprint(&self) -> String {
let duration = self.length.map(|d| d.as_secs_f64()).unwrap_or(0.0);
format!("Ogg Speex, {:.2} seconds", duration)
}
}
impl StreamInfo for SpeexInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
.and_then(|b| if b > 0 { Some(b as u32) } else { None })
}
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 }
}
#[derive(Debug, Default)]
pub struct SpeexTags {
pub inner: VCommentDict,
pub serial: u32,
pub padding: Vec<u8>,
}
impl std::ops::Deref for SpeexTags {
type Target = VCommentDict;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for SpeexTags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl crate::Tags for SpeexTags {
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 {
format!("SpeexTags({})", self.inner.keys().len())
}
fn module_name(&self) -> &'static str {
"oggspeex"
}
}
impl SpeexTags {
pub fn from_reader<R: Read + Seek>(reader: &mut R, serial: u32) -> Result<Self> {
let mut tags = SpeexTags {
inner: VCommentDict::new(),
serial,
padding: Vec::new(),
};
reader.seek(SeekFrom::Start(0))?;
let mut found_id = false;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
while let Ok(page) = OggPage::from_reader(reader) {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
if page.serial == serial {
found_id = true;
break;
}
}
if !found_id {
return Ok(tags);
}
let mut comment_pages = Vec::new();
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(reader) {
Ok(page) => page,
Err(_) => break,
};
if page.serial == serial {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Speex comment packet",
)?;
comment_pages.push(page);
if comment_pages.last().is_some_and(|p| p.is_complete()) {
break;
}
}
}
if comment_pages.is_empty() {
return Ok(tags);
}
let packets = OggPage::to_packets(&comment_pages, false)?;
if packets.is_empty() {
return Ok(tags);
}
let mut cursor = Cursor::new(&packets[0]);
match tags
.inner
.load(&mut cursor, crate::vorbis::ErrorMode::Strict, false)
{
Ok(_) => {
let pos = cursor.position() as usize;
if pos < packets[0].len() {
tags.padding = packets[0][pos..].to_vec();
}
}
Err(_) => {
}
}
Ok(tags)
}
pub fn inject<P: AsRef<Path>>(
&self,
path: P,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())?;
self.inject_writer(&mut file, padding_func)
}
pub fn inject_writer<F: Read + Write + Seek + 'static>(
&self,
file: &mut F,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
file.seek(SeekFrom::Start(0))?;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(SpeexHeaderError(SpeexError(
"No Speex stream found within page limit".to_string(),
))
.into());
}
let page = match OggPage::from_reader(file) {
Ok(page) => page,
Err(_) => {
return Err(
SpeexHeaderError(SpeexError("No Speex stream found".to_string())).into(),
);
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && first_packet.starts_with(b"Speex ") {
if page.serial != self.serial {
return Err(
SpeexHeaderError(SpeexError("Serial mismatch".to_string())).into()
);
}
break;
}
}
}
let page = loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(SpeexHeaderError(SpeexError(
"No comment packet found within page limit".to_string(),
))
.into());
}
let page = match OggPage::from_reader(file) {
Ok(page) => page,
Err(_) => {
return Err(SpeexHeaderError(SpeexError(
"No comment packet found".to_string(),
))
.into());
}
};
if page.serial == self.serial {
break page;
}
};
let mut old_pages = vec![page];
while !old_pages.last().is_none_or(|p| p.is_complete())
&& old_pages.last().is_some_and(|p| p.packets.len() <= 1)
&& old_pages.len() < MAX_PAGE_SEARCH
{
let page = match OggPage::from_reader(file) {
Ok(page) => page,
Err(_) => break,
};
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(SpeexHeaderError(SpeexError(
"No packets found in comment pages".to_string(),
))
.into());
}
let content_size = i64::try_from(file.seek(SeekFrom::End(0))?)
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(packets[0].len()).unwrap_or(0));
let mut comment_to_write = self.inner.clone();
if !comment_to_write.keys().is_empty() {
comment_to_write.set_vendor(format!("Audex {}", VERSION_STRING));
}
let mut vcomment_data = Vec::new();
comment_to_write.write(&mut vcomment_data, Some(false))?;
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;
if new_padding > 0 {
new_packets[0].extend_from_slice(&vec![0u8; usize::try_from(new_padding).unwrap_or(0)]);
}
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 last_position = old_pages
.last()
.ok_or_else(|| AudexError::InvalidData("no comment pages found".to_string()))?
.position;
let original_granule = if last_position < 0 {
0u64
} else {
last_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(file, &old_pages, final_pages)?;
Ok(())
}
}
#[derive(Debug)]
pub struct OggSpeex {
pub info: SpeexInfo,
pub tags: Option<SpeexTags>,
path: Option<std::path::PathBuf>,
}
impl OggSpeex {
pub fn new() -> Self {
Self {
info: SpeexInfo::default(),
tags: None,
path: None,
}
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(SpeexTags {
inner: VCommentDict::new(),
serial: self.info.serial,
padding: Vec::new(),
});
Ok(())
}
}
impl Default for OggSpeex {
fn default() -> Self {
Self::new()
}
}
impl FileType for OggSpeex {
type Tags = SpeexTags;
type Info = SpeexInfo;
fn format_id() -> &'static str {
"OggSpeex"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
debug_event!("parsing OGG Speex file");
let path_buf = path.as_ref().to_path_buf();
let file = File::open(&path_buf)?;
let mut reader = BufReader::new(file);
reader.seek(SeekFrom::Start(0))?;
let info = SpeexInfo::from_reader(&mut reader)?;
reader.seek(SeekFrom::Start(0))?;
let tags = SpeexTags::from_reader(&mut reader, info.serial)?;
debug_event!(tag_count = tags.keys().len(), "OGG Speex tags loaded");
Ok(Self {
info,
tags: Some(tags),
path: Some(path_buf),
})
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing OGG Speex file from reader");
let mut reader = reader;
reader.seek(SeekFrom::Start(0))?;
let info = SpeexInfo::from_reader(&mut reader)?;
reader.seek(SeekFrom::Start(0))?;
let tags = SpeexTags::from_reader(&mut reader, info.serial)?;
debug_event!(tag_count = tags.keys().len(), "OGG Speex tags loaded");
Ok(Self {
info,
tags: Some(tags),
path: None,
})
}
fn save(&mut self) -> Result<()> {
debug_event!("saving OGG Speex metadata");
let path = self.path.as_ref().ok_or_else(|| {
warn_event!("no file path available for OGG Speex save");
AudexError::InvalidOperation("No file path available for saving".to_string())
})?;
if let Some(ref mut tags) = self.tags {
tags.inject(path, None)?;
}
Ok(())
}
fn clear(&mut self) -> Result<()> {
let mut inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = SpeexTags {
inner,
serial: self.info.serial,
padding: Vec::new(),
};
let path = self.path.as_ref().ok_or_else(|| {
AudexError::InvalidOperation("No file path available for deletion".to_string())
})?;
empty_tags.inject(path, None)?;
self.tags = Some(empty_tags);
Ok(())
}
fn save_to_writer(&mut self, writer: &mut dyn crate::ReadWriteSeek) -> Result<()> {
if let Some(ref tags) = self.tags {
let data =
crate::util::read_all_from_writer_limited(writer, "in-memory Ogg Speex save")?;
let mut cursor = Cursor::new(data);
tags.inject_writer(&mut cursor, None)?;
let result = cursor.into_inner();
writer.seek(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 inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = SpeexTags {
inner,
serial: self.info.serial,
padding: Vec::new(),
};
let data = crate::util::read_all_from_writer_limited(writer, "in-memory Ogg Speex clear")?;
let mut cursor = Cursor::new(data);
empty_tags.inject_writer(&mut cursor, None)?;
let result = cursor.into_inner();
writer.seek(SeekFrom::Start(0))?;
writer.write_all(&result)?;
crate::util::truncate_writer_dyn(writer, result.len() as u64)?;
self.tags = Some(empty_tags);
Ok(())
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
if let Some(ref tags) = self.tags {
tags.inject(path, None)?;
}
Ok(())
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(SpeexTags {
inner: VCommentDict::new(),
serial: self.info.serial,
padding: Vec::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 {
&self.info
}
fn score(filename: &str, header: &[u8]) -> i32 {
let ogg_score = if header.starts_with(b"OggS") { 1 } else { 0 };
let speex_score = if header.windows(8).any(|window| window == b"Speex ") {
1
} else {
0
};
let ext_score = if filename.to_lowercase().ends_with(".spx") {
2
} else {
0
};
ogg_score * speex_score + ext_score
}
fn mime_types() -> &'static [&'static str] {
&["audio/x-speex"]
}
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
let mut speex = OggSpeex::load(path)?;
speex.clear()
}
#[cfg(feature = "async")]
impl OggSpeex {
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 info = Self::parse_info_async(&mut reader).await?;
let tags = Self::parse_tags_async(&mut reader, info.serial).await?;
Ok(Self {
info,
tags: Some(tags),
path: Some(path_buf),
})
}
async fn parse_info_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
) -> Result<SpeexInfo> {
let mut speex_info = SpeexInfo::default();
loop {
let page = match OggPage::from_reader_async(reader).await {
Ok(page) => page,
Err(_) => {
return Err(AudexError::InvalidData("No Speex stream found".to_string()));
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && &first_packet[0..8] == b"Speex " {
speex_info.serial = page.serial;
speex_info.parse_identification_packet(first_packet)?;
Self::post_tags_info_async(reader, &mut speex_info).await?;
return Ok(speex_info);
}
}
}
}
async fn post_tags_info_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
info: &mut SpeexInfo,
) -> Result<()> {
let last_page = match OggPage::find_last_async(reader, info.serial, true).await? {
Some(page) => page,
None => return Ok(()),
};
if info.sample_rate > 0 && last_page.position > 0 {
let duration_secs = last_page.position as f64 / info.sample_rate as f64;
if duration_secs.is_finite() && duration_secs >= 0.0 && duration_secs <= u64::MAX as f64
{
info.length = Some(std::time::Duration::from_secs_f64(duration_secs));
}
}
Ok(())
}
async fn parse_tags_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
serial: u32,
) -> Result<SpeexTags> {
let mut tags = SpeexTags {
inner: VCommentDict::new(),
serial,
padding: Vec::new(),
};
reader.seek(SeekFrom::Start(0)).await?;
let mut pages = Vec::new();
let mut found_header = false;
let mut found_tags = false;
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
loop {
let page = match OggPage::from_reader_async(reader).await {
Ok(page) => page,
Err(_) => break,
};
if page.serial == serial {
if !found_header {
found_header = true;
continue;
}
if !found_tags {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Speex comment packet",
)?;
pages.push(page);
found_tags = true;
} else if !pages.last().is_none_or(|p| p.is_complete()) {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Speex comment packet",
)?;
pages.push(page);
} else {
break;
}
}
}
if pages.is_empty() {
return Ok(tags);
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() {
return Ok(tags);
}
let comment_data = &packets[0];
let mut cursor = Cursor::new(comment_data);
match tags
.inner
.load(&mut cursor, crate::vorbis::ErrorMode::Replace, false)
{
Ok(_) => {
let pos = cursor.position() as usize;
if pos < comment_data.len() {
tags.padding = comment_data[pos..].to_vec();
}
}
Err(_) => {
tags.inner = VCommentDict::new();
tags.padding = comment_data.to_vec();
}
}
Ok(tags)
}
pub async fn save_async(&mut self) -> Result<()> {
let path = self.path.as_ref().ok_or_else(|| {
AudexError::InvalidOperation("No file path available for saving".to_string())
})?;
if let Some(ref tags) = self.tags {
Self::inject_tags_async(path, tags, None).await?;
}
Ok(())
}
async fn inject_tags_async<P: AsRef<Path>>(
path: P,
tags: &SpeexTags,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
let file_path = path.as_ref();
let file = TokioOpenOptions::new()
.read(true)
.write(true)
.open(file_path)
.await?;
let mut reader = TokioBufReader::new(file);
let mut comment_pages = Vec::new();
let mut found_header = false;
let mut found_tags = false;
reader.seek(SeekFrom::Start(0)).await?;
loop {
let page = match OggPage::from_reader_async(&mut reader).await {
Ok(page) => page,
Err(_) => break,
};
if page.serial == tags.serial {
if !found_header {
found_header = true;
continue;
}
if !found_tags {
comment_pages.push(page);
found_tags = true;
} else if !comment_pages.last().is_none_or(|p| p.is_complete()) {
comment_pages.push(page);
} else {
break;
}
}
}
if comment_pages.is_empty() {
return Err(AudexError::InvalidData(
"No comment packet found".to_string(),
));
}
let old_packets = OggPage::to_packets(&comment_pages, false)?;
if old_packets.is_empty() {
return Err(AudexError::InvalidData(
"No packets found in comment pages".to_string(),
));
}
let content_size = {
let old_pos = reader.stream_position().await?;
let file_size = reader.seek(SeekFrom::End(0)).await?;
reader.seek(SeekFrom::Start(old_pos)).await?;
i64::try_from(file_size)
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(old_packets[0].len()).unwrap_or(0))
};
let mut comment_to_write = tags.inner.clone();
if !comment_to_write.keys().is_empty() {
comment_to_write.set_vendor(format!("Audex {}", VERSION_STRING));
}
let mut vcomment_data = Vec::new();
comment_to_write.write(&mut vcomment_data, Some(false))?;
let padding_left = old_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 = old_packets;
new_packets[0] = vcomment_data;
if new_padding > 0 {
new_packets[0].extend_from_slice(&vec![0u8; usize::try_from(new_padding).unwrap_or(0)]);
}
let new_pages = OggPage::from_packets_try_preserve(new_packets, &comment_pages);
drop(reader);
let mut writer = TokioOpenOptions::new()
.read(true)
.write(true)
.open(file_path)
.await?;
OggPage::replace_async(&mut writer, &comment_pages, new_pages).await?;
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
let mut inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = SpeexTags {
inner,
serial: self.info.serial,
padding: Vec::new(),
};
let path = self.path.as_ref().ok_or_else(|| {
AudexError::InvalidOperation("No file path available for deletion".to_string())
})?;
Self::inject_tags_async(path, &empty_tags, None).await?;
self.tags = Some(empty_tags);
Ok(())
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
let mut speex = Self::load_async(path).await?;
speex.clear_async().await
}
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
OggSpeex::delete_async(path).await
}