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, OpenOptions};
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 OpusError(pub String);
impl std::fmt::Display for OpusError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::error::Error for OpusError {}
#[derive(Debug, Clone, PartialEq)]
pub struct OpusHeaderError(pub OpusError);
impl std::fmt::Display for OpusHeaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Opus header error: {}", self.0)
}
}
impl std::error::Error for OpusHeaderError {}
impl From<OpusError> for OpusHeaderError {
fn from(err: OpusError) -> Self {
OpusHeaderError(err)
}
}
impl From<OpusHeaderError> for AudexError {
fn from(err: OpusHeaderError) -> Self {
AudexError::InvalidData(err.to_string())
}
}
impl From<OpusError> for AudexError {
fn from(err: OpusError) -> Self {
AudexError::InvalidData(err.0)
}
}
#[derive(Debug, Clone, Default)]
pub struct OpusInfo {
pub length: Option<Duration>,
pub channels: u16,
pub sample_rate: u32, pub pre_skip: u16,
pub version: u8,
pub gain: i16,
pub channel_mapping_family: u8,
pub stream_count: Option<u8>,
pub coupled_stream_count: Option<u8>,
pub channel_mapping: Option<Vec<u8>>,
pub serial: u32,
}
impl OpusInfo {
pub fn from_reader<R: Read + Seek>(reader: &mut R) -> Result<Self> {
let mut opus_info = Self {
sample_rate: 48000, ..Default::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(OpusHeaderError(OpusError(
"No Opus stream found within page limit".to_string(),
))
.into());
}
let page = match OggPage::from_reader(reader) {
Ok(page) => page,
Err(_) => {
return Err(
OpusHeaderError(OpusError("No Opus stream found".to_string())).into(),
);
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && &first_packet[0..8] == b"OpusHead" {
opus_info.serial = page.serial;
opus_info.parse_head_packet(first_packet)?;
opus_info.post_tags(reader)?;
return Ok(opus_info);
}
}
}
}
fn parse_head_packet(&mut self, packet: &[u8]) -> Result<()> {
if packet.len() < 19 {
return Err(
OpusHeaderError(OpusError("Invalid OpusHead packet length".to_string())).into(),
);
}
let mut cursor = Cursor::new(&packet[8..]);
self.version = cursor.read_u8()?;
self.channels = cursor.read_u8()? as u16;
self.pre_skip = cursor.read_u16::<LittleEndian>()?;
let _sample_rate = cursor.read_u32::<LittleEndian>()?; self.gain = cursor.read_i16::<LittleEndian>()?;
self.channel_mapping_family = cursor.read_u8()?;
if self.version >> 4 != 0 {
return Err(OpusHeaderError(OpusError(format!(
"Unsupported Opus version: {}",
self.version >> 4
)))
.into());
}
if self.channels == 0 || self.channels > 255 {
return Err(OpusHeaderError(OpusError(format!(
"Invalid channel count: {}",
self.channels
)))
.into());
}
if self.channel_mapping_family > 0 {
if packet.len() < 21 {
return Err(OpusHeaderError(OpusError(
"Insufficient data for channel mapping".to_string(),
))
.into());
}
self.stream_count = Some(cursor.read_u8()?);
self.coupled_stream_count = Some(cursor.read_u8()?);
let expected_mapping_len = self.channels as usize;
if packet.len() < 21 + expected_mapping_len {
return Err(OpusHeaderError(OpusError(
"Insufficient channel mapping data".to_string(),
))
.into());
}
let mut channel_mapping = vec![0u8; expected_mapping_len];
cursor.read_exact(&mut channel_mapping)?;
self.channel_mapping = Some(channel_mapping);
}
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 {
let effective_samples =
(last_page.position as u64).saturating_sub(self.pre_skip as u64);
let duration_secs = effective_samples as f64 / 48000.0;
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!(
"Opus, {:.2} seconds, {} channel(s), {} Hz",
duration, self.channels, self.sample_rate
)
}
}
impl StreamInfo for OpusInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
None
}
fn sample_rate(&self) -> Option<u32> {
Some(self.sample_rate) }
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 OpusTags {
pub inner: VCommentDict,
pub serial: u32,
pub padding: i32, pub pad_data: Vec<u8>, }
impl std::ops::Deref for OpusTags {
type Target = VCommentDict;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for OpusTags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl crate::Tags for OpusTags {
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!("OpusTags({})", self.inner.keys().len())
}
fn module_name(&self) -> &'static str {
"oggopus"
}
}
impl OpusTags {
pub fn from_reader<R: Read + Seek>(reader: &mut R, serial: u32) -> Result<Self> {
let mut tags = OpusTags {
inner: VCommentDict::new(),
serial,
padding: 0,
pad_data: Vec::new(),
};
reader.seek(SeekFrom::Start(0))?;
let mut pages = Vec::new();
let mut found_tags = false;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
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 {
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && first_packet.starts_with(b"OpusTags") {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
found_tags = true;
} else if found_tags {
let last_complete = pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
} else {
break;
}
}
} else if found_tags {
let last_complete = pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
}
}
}
}
if pages.is_empty() {
return Ok(tags);
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 8 {
return Ok(tags);
}
if &packets[0][0..8] == b"OpusTags" {
let comment_data = &packets[0][8..];
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() {
let remaining = &comment_data[pos..];
tags.padding = remaining.len().min(i32::MAX as usize) as i32;
if !remaining.is_empty() && (remaining[0] & 0x1) == 1 {
tags.pad_data = remaining.to_vec();
tags.padding = 0; }
}
}
Err(_) => {
tags.inner = VCommentDict::new();
tags.padding = comment_data.len().min(i32::MAX as usize) as i32;
}
}
}
Ok(tags)
}
pub fn inject<P: AsRef<Path>>(&self, path: P) -> Result<()> {
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())?;
self.inject_writer(&mut file)
}
pub fn inject_writer<F: Read + Write + Seek + 'static>(&self, file: &mut F) -> Result<()> {
let mut comment_pages = Vec::new();
let mut found_tags = false;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
file.seek(SeekFrom::Start(0))?;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(file) {
Ok(page) => page,
Err(_) => break,
};
if page.serial == self.serial {
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && first_packet.starts_with(b"OpusTags") {
comment_pages.push(page);
found_tags = true;
} else if found_tags {
let last_complete = comment_pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
comment_pages.push(page);
} else {
break;
}
}
}
}
}
if comment_pages.is_empty() {
return Err(AudexError::InvalidData(
"No OpusTags header found".to_string(),
));
}
let old_packets = OggPage::to_packets(&comment_pages, false)?;
if old_packets.is_empty() {
return Err(AudexError::InvalidData("No packets found".to_string()));
}
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 = b"OpusTags".to_vec();
let mut vcomment_bytes = Vec::new();
comment_to_write.write(&mut vcomment_bytes, Some(false))?;
vcomment_data.extend_from_slice(&vcomment_bytes);
let mut new_packets = old_packets;
if !self.pad_data.is_empty() {
new_packets[0] = vcomment_data;
new_packets[0].extend_from_slice(&self.pad_data);
} else {
let content_size = {
let old_pos = file.stream_position()?;
let file_size = file.seek(SeekFrom::End(0))?;
file.seek(SeekFrom::Start(old_pos))?;
i64::try_from(file_size)
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(new_packets[0].len()).unwrap_or(0))
};
let padding_left = new_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(None::<fn(&crate::tags::PaddingInfo) -> i64>);
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(), &comment_pages);
let final_pages = if new_pages.is_empty() {
let first_sequence = comment_pages[0].sequence;
let last_position = comment_pages
.last()
.ok_or_else(|| AudexError::InvalidData("no comment pages collected".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
};
OggPage::replace(file, &comment_pages, final_pages)?;
Ok(())
}
}
#[derive(Debug)]
pub struct OggOpus {
pub info: OpusInfo,
pub tags: Option<OpusTags>,
path: Option<std::path::PathBuf>,
}
impl OggOpus {
pub fn new() -> Self {
Self {
info: OpusInfo::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(OpusTags {
inner: VCommentDict::new(),
serial: self.info.serial,
padding: 0,
pad_data: Vec::new(),
});
Ok(())
}
}
impl Default for OggOpus {
fn default() -> Self {
Self::new()
}
}
impl FileType for OggOpus {
type Tags = OpusTags;
type Info = OpusInfo;
fn format_id() -> &'static str {
"OggOpus"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
debug_event!("parsing OGG Opus 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 = OpusInfo::from_reader(&mut reader)?;
let tags = OpusTags::from_reader(&mut reader, info.serial)?;
debug_event!(tag_count = tags.keys().len(), "OGG Opus 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 Opus file from reader");
let mut reader = reader;
reader.seek(SeekFrom::Start(0))?;
let info = OpusInfo::from_reader(&mut reader)?;
let tags = OpusTags::from_reader(&mut reader, info.serial)?;
debug_event!(tag_count = tags.keys().len(), "OGG Opus tags loaded");
Ok(Self {
info,
tags: Some(tags),
path: None,
})
}
fn save(&mut self) -> Result<()> {
debug_event!("saving OGG Opus metadata");
let path = self.path.as_ref().ok_or_else(|| {
warn_event!("no file path available for OGG Opus save");
AudexError::InvalidOperation("No file path available for saving".to_string())
})?;
if let Some(ref tags) = self.tags {
tags.inject(path)?;
}
Ok(())
}
fn clear(&mut self) -> Result<()> {
let mut inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = OpusTags {
inner,
serial: self.info.serial,
padding: 0,
pad_data: 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)?;
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 Opus save")?;
let mut cursor = Cursor::new(data);
tags.inject_writer(&mut cursor)?;
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 = OpusTags {
inner,
serial: self.info.serial,
padding: 0,
pad_data: Vec::new(),
};
let data = crate::util::read_all_from_writer_limited(writer, "in-memory Ogg Opus clear")?;
let mut cursor = Cursor::new(data);
empty_tags.inject_writer(&mut cursor)?;
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)?;
}
Ok(())
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(OpusTags {
inner: VCommentDict::new(),
serial: self.info.serial,
padding: 0,
pad_data: 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 opus_head_score = if header.windows(8).any(|window| window == b"OpusHead") {
1
} else {
0
};
let ogg_score = if header.windows(4).any(|window| window == b"OggS") {
1
} else {
0
};
opus_head_score + ogg_score
}
fn mime_types() -> &'static [&'static str] {
&["audio/ogg", "audio/ogg; codecs=opus"]
}
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
let mut opus = OggOpus::load(path)?;
opus.clear()
}
#[cfg(feature = "async")]
impl OggOpus {
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<OpusInfo> {
let mut opus_info = OpusInfo {
sample_rate: 48000, ..Default::default()
};
loop {
let page = match OggPage::from_reader_async(reader).await {
Ok(page) => page,
Err(_) => {
return Err(AudexError::InvalidData("No Opus stream found".to_string()));
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && &first_packet[0..8] == b"OpusHead" {
opus_info.serial = page.serial;
opus_info.parse_head_packet(first_packet)?;
Self::post_tags_info_async(reader, &mut opus_info).await?;
return Ok(opus_info);
}
}
}
}
async fn post_tags_info_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
info: &mut OpusInfo,
) -> Result<()> {
let last_page = match OggPage::find_last_async(reader, info.serial, true).await? {
Some(page) => page,
None => return Ok(()),
};
if last_page.position > 0 {
let effective_samples =
(last_page.position as u64).saturating_sub(info.pre_skip as u64);
let duration_secs = effective_samples as f64 / 48000.0;
if duration_secs.is_finite() && duration_secs >= 0.0 && duration_secs <= u64::MAX as f64
{
info.length = Some(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<OpusTags> {
let mut tags = OpusTags {
inner: VCommentDict::new(),
serial,
padding: 0,
pad_data: Vec::new(),
};
reader.seek(SeekFrom::Start(0)).await?;
let mut pages = Vec::new();
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 let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && first_packet.starts_with(b"OpusTags") {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
found_tags = true;
} else if found_tags {
let last_complete = pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
} else {
break;
}
}
} else if found_tags {
let last_complete = pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Opus comment packet",
)?;
pages.push(page);
}
}
}
}
if pages.is_empty() {
return Ok(tags);
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 8 {
return Ok(tags);
}
if &packets[0][0..8] == b"OpusTags" {
let comment_data = &packets[0][8..];
let mut cursor = std::io::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() {
let remaining = &comment_data[pos..];
tags.padding = remaining.len().min(i32::MAX as usize) as i32;
if !remaining.is_empty() && (remaining[0] & 0x1) == 1 {
tags.pad_data = remaining.to_vec();
tags.padding = 0;
}
}
}
Err(_) => {
tags.inner = VCommentDict::new();
tags.padding = comment_data.len().min(i32::MAX as usize) as i32;
}
}
}
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).await?;
}
Ok(())
}
async fn inject_tags_async<P: AsRef<Path>>(path: P, tags: &OpusTags) -> 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_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 let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 8 && first_packet.starts_with(b"OpusTags") {
comment_pages.push(page);
found_tags = true;
} else if found_tags {
let last_complete = comment_pages
.last()
.ok_or_else(|| {
AudexError::InvalidData(
"expected non-empty page list after tag header".into(),
)
})?
.is_complete();
if !last_complete {
comment_pages.push(page);
} else {
break;
}
}
}
}
}
if comment_pages.is_empty() {
return Err(AudexError::InvalidData(
"No OpusTags header found".to_string(),
));
}
let old_packets = OggPage::to_packets(&comment_pages, false)?;
if old_packets.is_empty() {
return Err(AudexError::InvalidData("No packets found".to_string()));
}
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 = b"OpusTags".to_vec();
let mut vcomment_bytes = Vec::new();
comment_to_write.write(&mut vcomment_bytes, Some(false))?;
vcomment_data.extend_from_slice(&vcomment_bytes);
let mut new_packets = old_packets;
if !tags.pad_data.is_empty() {
new_packets[0] = vcomment_data;
new_packets[0].extend_from_slice(&tags.pad_data);
} else {
let content_size = {
let file_meta = tokio::fs::metadata(file_path).await?;
i64::try_from(file_meta.len())
.unwrap_or(i64::MAX)
.saturating_sub(i64::try_from(new_packets[0].len()).unwrap_or(0))
};
let padding_left = new_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(None::<fn(&crate::tags::PaddingInfo) -> i64>);
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(), &comment_pages);
let final_pages = if new_pages.is_empty() {
let first_sequence = comment_pages[0].sequence;
let last_position = comment_pages
.last()
.ok_or_else(|| AudexError::InvalidData("no comment pages collected".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
};
drop(reader);
let mut writer = TokioOpenOptions::new()
.read(true)
.write(true)
.open(file_path)
.await?;
OggPage::replace_async(&mut writer, &comment_pages, final_pages).await?;
Ok(())
}
pub async fn clear_async(&mut self) -> Result<()> {
let mut inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = OpusTags {
inner,
serial: self.info.serial,
padding: 0,
pad_data: 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).await?;
self.tags = Some(empty_tags);
Ok(())
}
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
let mut opus = Self::load_async(path).await?;
opus.clear_async().await
}
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
OggOpus::delete_async(path).await
}