use crate::VERSION_STRING;
use crate::ogg::OggPage;
use crate::vorbis::VCommentDict;
use crate::{AudexError, FileType, Result, StreamInfo};
use byteorder::{BigEndian, ReadBytesExt};
use std::io::{Cursor, Seek};
use std::path::Path;
use std::time::Duration;
#[cfg(feature = "async")]
use std::io::SeekFrom;
#[cfg(feature = "async")]
use tokio::fs::{File as TokioFile, OpenOptions as TokioOpenOptions};
#[cfg(feature = "async")]
use tokio::io::{AsyncSeekExt, BufReader as TokioBufReader};
#[derive(Debug, Default)]
pub struct TheoraTags {
pub inner: VCommentDict,
pub serial: u32,
}
impl std::ops::Deref for TheoraTags {
type Target = VCommentDict;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl std::ops::DerefMut for TheoraTags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl crate::Tags for TheoraTags {
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!("TheoraTags({})", self.inner.keys().len())
}
fn module_name(&self) -> &'static str {
"oggtheora"
}
}
#[derive(Debug)]
pub struct OggTheora {
pub info: TheoraInfo,
pub tags: Option<TheoraTags>,
pub path: Option<std::path::PathBuf>,
}
#[derive(Debug, Clone)]
pub struct TheoraInfo {
pub length: Option<Duration>,
pub fps: f64,
pub bitrate: u32,
pub width: u32,
pub height: u32,
pub serial: u32,
pub granule_shift: u8,
pub version_major: u8,
pub version_minor: u8,
pub frame_width: u32,
pub frame_height: u32,
pub offset_x: u32,
pub offset_y: u32,
pub aspect_numerator: u32,
pub aspect_denominator: u32,
pub colorspace: u8,
pub pixel_fmt: u8,
pub target_bitrate: u32,
pub quality: u8,
pub keyframe_granule_shift: u8,
}
impl Default for TheoraInfo {
fn default() -> Self {
Self {
length: None,
fps: 0.0,
bitrate: 0,
width: 0,
height: 0,
serial: 0,
granule_shift: 0,
version_major: 0,
version_minor: 0,
frame_width: 0,
frame_height: 0,
offset_x: 0,
offset_y: 0,
aspect_numerator: 0,
aspect_denominator: 0,
colorspace: 0,
pixel_fmt: 0,
target_bitrate: 0,
quality: 0,
keyframe_granule_shift: 0,
}
}
}
impl StreamInfo for TheoraInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
if self.bitrate > 0 {
Some(self.bitrate)
} else {
None
}
}
fn sample_rate(&self) -> Option<u32> {
None }
fn channels(&self) -> Option<u16> {
None }
fn bits_per_sample(&self) -> Option<u16> {
None }
}
impl TheoraInfo {
pub fn from_identification_header(packet: &[u8]) -> Result<Self> {
if packet.len() < 42 {
return Err(AudexError::InvalidData(
"Theora identification header too short".to_string(),
));
}
if packet[0] != 0x80 || &packet[1..7] != b"theora" {
return Err(AudexError::InvalidData(
"Invalid Theora identification header".to_string(),
));
}
let mut cursor = Cursor::new(&packet[7..]);
let version_major = cursor.read_u8()?;
let version_minor = cursor.read_u8()?;
let _version_subminor = cursor.read_u8()?;
if version_major != 3 || version_minor < 2 {
return Err(AudexError::UnsupportedFormat(format!(
"Found Theora version {}.{}, expected major 3 with minor >= 2",
version_major, version_minor
)));
}
let frame_width = (cursor.read_u16::<BigEndian>()? as u32) << 4;
let frame_height = (cursor.read_u16::<BigEndian>()? as u32) << 4;
let width = read_u24_be(&mut cursor)?;
let height = read_u24_be(&mut cursor)?;
let offset_x = cursor.read_u8()? as u32;
let offset_y = cursor.read_u8()? as u32;
let fps_numerator = cursor.read_u32::<BigEndian>()?;
let fps_denominator = cursor.read_u32::<BigEndian>()?;
if fps_denominator == 0 || fps_numerator == 0 {
return Err(AudexError::InvalidData(
"Frame rate numerator or denominator is zero".to_string(),
));
}
let fps = fps_numerator as f64 / fps_denominator as f64;
let aspect_numerator = read_u24_be(&mut cursor)?;
let aspect_denominator = read_u24_be(&mut cursor)?;
let colorspace = cursor.read_u8()?;
let bitrate_bytes = read_u24_be(&mut cursor)?;
let quality_keyframe = cursor.read_u16::<BigEndian>()?;
let quality = (quality_keyframe >> 10) as u8; let keyframe_granule_shift = (quality_keyframe >> 5) as u8 & 0x1F; let pixel_fmt = ((quality_keyframe >> 3) & 0x03) as u8;
if keyframe_granule_shift == 0 {
return Err(crate::AudexError::InvalidData(
"Theora keyframe granule shift must be 1-31, got 0".to_string(),
));
}
Ok(Self {
length: None, fps,
bitrate: bitrate_bytes,
width,
height,
serial: 0, granule_shift: keyframe_granule_shift,
version_major,
version_minor,
frame_width,
frame_height,
offset_x,
offset_y,
aspect_numerator,
aspect_denominator,
colorspace,
pixel_fmt,
target_bitrate: bitrate_bytes,
quality,
keyframe_granule_shift,
})
}
pub fn set_length(&mut self, position: i64) {
if self.fps > 0.0 && position > 0 && position != -1 {
let granule_position = position as u64;
let mask = (1u64 << self.granule_shift) - 1;
let keyframe_count = granule_position >> self.granule_shift;
let frames_since_keyframe = granule_position & mask;
let total_frames = keyframe_count + frames_since_keyframe;
let duration_secs = total_frames as f64 / self.fps;
if duration_secs.is_finite() && duration_secs >= 0.0 && duration_secs <= u64::MAX as f64
{
self.length = Some(Duration::from_secs_f64(duration_secs));
}
}
}
pub fn pretty_print(&self) -> String {
let duration = self
.length
.map(|d| format!("{:.2}", d.as_secs_f64()))
.unwrap_or_else(|| "unknown".to_string());
format!("Ogg Theora, {} seconds, {} bps", duration, self.bitrate)
}
}
pub fn read_u24_be<R: ReadBytesExt>(reader: &mut R) -> std::io::Result<u32> {
let mut buf = [0u8; 3];
reader.read_exact(&mut buf)?;
Ok(((buf[0] as u32) << 16) | ((buf[1] as u32) << 8) | (buf[2] as u32))
}
impl FileType for OggTheora {
type Tags = TheoraTags;
type Info = TheoraInfo;
fn format_id() -> &'static str {
"OggTheora"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
use std::fs::File;
use std::io::BufReader;
debug_event!("parsing OGG Theora 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 theora_info = None;
let mut theora_serial = None;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(&mut reader) {
Ok(page) => page,
Err(e) => {
if let AudexError::Io(io_err) = &e {
if io_err.kind() == std::io::ErrorKind::UnexpectedEof {
break;
}
}
return Err(e);
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7 && first_packet.starts_with(b"\x80theora") {
if !page.is_first() {
return Err(AudexError::InvalidData(
"Theora identification header not on first page".to_string(),
));
}
let mut info = TheoraInfo::from_identification_header(first_packet)?;
info.serial = page.serial;
theora_info = Some(info);
theora_serial = Some(page.serial);
break;
}
}
}
let mut info = theora_info
.ok_or_else(|| AudexError::InvalidData("No Theora stream found".to_string()))?;
let serial = theora_serial
.ok_or_else(|| AudexError::InvalidData("No Theora serial number found".to_string()))?;
let mut tags = None;
let mut found_comment = false;
let mut comment_pages = Vec::new();
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
pages_read = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(&mut reader) {
Ok(page) => page,
Err(_) => break,
};
if page.serial == serial {
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7 && first_packet.starts_with(b"\x81theora") {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
found_comment = true;
} else if found_comment {
if !comment_pages.last().is_none_or(|p| p.is_complete()) {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
} else {
break; }
}
}
}
}
if !comment_pages.is_empty() {
let packets = OggPage::to_packets(&comment_pages, false)?;
if let Some(comment_packet) = packets.first() {
if comment_packet.len() > 7 {
let comment_data = &comment_packet[7..];
let vcomment = VCommentDict::from_bytes_with_options(
comment_data,
crate::vorbis::ErrorMode::Strict,
false,
)?;
tags = Some(TheoraTags {
inner: vcomment,
serial,
});
}
}
}
reader.seek(std::io::SeekFrom::Start(0))?;
let last_page = OggPage::find_last(&mut reader, serial, true)?
.ok_or_else(|| AudexError::InvalidData("could not find last page".to_string()))?;
if last_page.position > 0 && last_page.position != -1 {
info.set_length(last_page.position);
}
if let Some(ref _t) = tags {
debug_event!(tag_count = _t.keys().len(), "OGG Theora tags loaded");
}
Ok(Self {
info,
tags,
path: Some(path_buf),
})
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing OGG Theora file from reader");
let mut reader = reader;
reader.seek(std::io::SeekFrom::Start(0))?;
let mut theora_info = None;
let mut theora_serial = None;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(&mut reader) {
Ok(page) => page,
Err(e) => {
if let AudexError::Io(io_err) = &e {
if io_err.kind() == std::io::ErrorKind::UnexpectedEof {
break;
}
}
return Err(e);
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7 && first_packet.starts_with(b"\x80theora") {
if !page.is_first() {
return Err(AudexError::InvalidData(
"Theora identification header not on first page".to_string(),
));
}
let mut info = TheoraInfo::from_identification_header(first_packet)?;
info.serial = page.serial;
theora_info = Some(info);
theora_serial = Some(page.serial);
break;
}
}
}
let mut info = theora_info
.ok_or_else(|| AudexError::InvalidData("No Theora stream found".to_string()))?;
let serial = theora_serial
.ok_or_else(|| AudexError::InvalidData("No Theora serial number found".to_string()))?;
let mut tags = None;
let mut found_comment = false;
let mut comment_pages = Vec::new();
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
pages_read = 0;
loop {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
break;
}
let page = match OggPage::from_reader(&mut reader) {
Ok(page) => page,
Err(_) => break,
};
if page.serial == serial {
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7 && first_packet.starts_with(b"\x81theora") {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
found_comment = true;
} else if found_comment {
if !comment_pages.last().is_none_or(|p| p.is_complete()) {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
} else {
break; }
}
}
}
}
if !comment_pages.is_empty() {
let packets = OggPage::to_packets(&comment_pages, false)?;
if let Some(comment_packet) = packets.first() {
if comment_packet.len() > 7 {
let comment_data = &comment_packet[7..];
let vcomment = VCommentDict::from_bytes_with_options(
comment_data,
crate::vorbis::ErrorMode::Strict,
false,
)?;
tags = Some(TheoraTags {
inner: vcomment,
serial,
});
}
}
}
reader.seek(std::io::SeekFrom::Start(0))?;
let last_page = OggPage::find_last(&mut reader, serial, true)?
.ok_or_else(|| AudexError::InvalidData("could not find last page".to_string()))?;
if last_page.position > 0 && last_page.position != -1 {
info.set_length(last_page.position);
}
Ok(Self {
info,
tags,
path: None,
})
}
fn save(&mut self) -> Result<()> {
debug_event!("saving OGG Theora metadata");
let path = self.path.as_ref().ok_or_else(|| {
warn_event!("no file path available for OGG Theora save");
AudexError::InvalidOperation("No file path available for saving".to_string())
})?;
if let Some(ref tags) = self.tags {
self.inject_tags(path, tags)?;
}
Ok(())
}
fn clear(&mut self) -> Result<()> {
let mut inner = VCommentDict::new();
inner.set_vendor(String::new());
let empty_tags = TheoraTags {
inner,
serial: self.info.serial,
};
let path = self.path.as_ref().ok_or_else(|| {
AudexError::InvalidOperation("No file path available for deletion".to_string())
})?;
self.inject_tags(path, &empty_tags)?;
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 Theora save")?;
let mut cursor = std::io::Cursor::new(data);
self.inject_theora_tags(&mut cursor, tags)?;
let result = cursor.into_inner();
writer.seek(std::io::SeekFrom::Start(0))?;
std::io::Write::write_all(writer, &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 = TheoraTags {
inner,
serial: self.info.serial,
};
let data = crate::util::read_all_from_writer_limited(writer, "in-memory Ogg Theora clear")?;
let mut cursor = std::io::Cursor::new(data);
self.inject_theora_tags(&mut cursor, &empty_tags)?;
let result = cursor.into_inner();
writer.seek(std::io::SeekFrom::Start(0))?;
std::io::Write::write_all(writer, &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 {
self.inject_tags(path, tags)?;
}
Ok(())
}
fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(TheoraTags {
inner: VCommentDict::new(),
serial: self.info.serial,
});
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 mut score = 0;
if header.len() >= 4 && &header[0..4] == b"OggS" {
score += 1;
} else {
let lower_filename = filename.to_lowercase();
if lower_filename.ends_with(".ogv")
&& !header.is_empty()
&& header.len() >= 4
&& !header.starts_with(b"MP3")
&& !header.starts_with(b"ID3")
{
return 1; }
return 0; }
if header.len() >= 11 {
if header.windows(7).any(|window| window == b"\x80theora") {
score += 2; }
if header.windows(7).any(|window| window == b"\x81theora") {
score += 2; }
}
let lower_filename = filename.to_lowercase();
if lower_filename.ends_with(".ogv") {
score += 2; } else if lower_filename.ends_with(".ogg") && score > 1 {
score += 1; }
score
}
fn mime_types() -> &'static [&'static str] {
&["video/x-theora", "video/ogg"]
}
}
impl OggTheora {
fn inject_tags<P: AsRef<Path>>(&self, path: P, tags: &TheoraTags) -> Result<()> {
use std::fs::OpenOptions;
let mut file = OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())?;
self.inject_theora_tags(&mut file, tags)
}
fn inject_theora_tags<F: std::io::Read + std::io::Write + std::io::Seek + 'static>(
&self,
file: &mut F,
tags: &TheoraTags,
) -> Result<()> {
use std::io::SeekFrom;
let serial = self.info.serial;
let mut comment_pages = Vec::new();
let mut found_comment = false;
let mut cumulative_bytes = 0u64;
let limits = crate::limits::ParseLimits::default();
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 == serial {
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7 && first_packet.starts_with(b"\x81theora") {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
found_comment = true;
} else if found_comment {
if !comment_pages.last().is_none_or(|p| p.is_complete())
&& comment_pages.last().is_some_and(|p| p.packets.len() <= 1)
{
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
comment_pages.push(page);
} else {
break;
}
}
}
}
}
if comment_pages.is_empty() {
return Err(AudexError::InvalidData(
"No Theora comment header found".to_string(),
));
}
let packets = OggPage::to_packets(&comment_pages, false)?;
if packets.is_empty() {
return Err(AudexError::InvalidData(
"Failed to reconstruct comment packet".to_string(),
));
}
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(packets[0].len()).unwrap_or(0))
};
let vcomment_data = {
let mut data = b"\x81theora".to_vec();
let mut vcomment_bytes = Vec::new();
let mut comment_to_write = tags.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(false))?;
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(None::<fn(&crate::tags::PaddingInfo) -> i64>);
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(), &comment_pages);
let final_pages = if new_pages.is_empty() {
let first_sequence = comment_pages[0].sequence;
let original_granule = comment_pages
.last()
.map(|p| {
if p.position < 0 {
0u64
} else {
p.position as u64
}
})
.unwrap_or(0);
OggPage::from_packets_with_options(
new_packets,
first_sequence,
4096,
2048,
original_granule,
)?
} else {
new_pages
};
OggPage::replace(file, &comment_pages, final_pages)?;
Ok(())
}
pub fn add_tags(&mut self) -> Result<()> {
if self.tags.is_some() {
return Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
));
}
self.tags = Some(TheoraTags {
inner: VCommentDict::new(),
serial: self.info.serial,
});
Ok(())
}
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
let mut theora = OggTheora::load(path)?;
theora.clear()
}
#[cfg(feature = "async")]
impl OggTheora {
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<TheoraInfo> {
loop {
let page = match OggPage::from_reader_async(reader).await {
Ok(page) => page,
Err(_) => {
return Err(AudexError::InvalidData(
"No Theora stream found".to_string(),
));
}
};
if let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7
&& first_packet[0] == 0x80
&& &first_packet[1..7] == b"theora"
{
let mut info = TheoraInfo::from_identification_header(first_packet)?;
info.serial = page.serial;
Self::post_tags_info_async(reader, &mut info).await?;
return Ok(info);
}
}
}
}
async fn post_tags_info_async<R: tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin>(
reader: &mut R,
info: &mut TheoraInfo,
) -> 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 != -1 && info.fps > 0.0 {
let granule_shift = info.keyframe_granule_shift;
let granule = last_page.position as u64;
let keyframe = granule >> granule_shift;
let offset = granule & ((1u64 << granule_shift) - 1);
let total_frames = keyframe + offset;
let duration_secs = total_frames as f64 / info.fps;
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<TheoraTags> {
let mut tags = TheoraTags {
inner: VCommentDict::new(),
serial,
};
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 let Some(first_packet) = page.packets.first() {
if first_packet.len() >= 7
&& first_packet[0] == 0x81
&& &first_packet[1..7] == b"theora"
{
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
pages.push(page);
found_tags = true;
} else if !found_header
&& first_packet.len() >= 7
&& first_packet[0] == 0x80
&& &first_packet[1..7] == b"theora"
{
found_header = true;
} else if found_tags && !pages.last().is_none_or(|p| p.is_complete()) {
OggPage::accumulate_page_bytes_with_limit(
limits,
&mut cumulative_bytes,
&page,
"Ogg Theora comment packet",
)?;
pages.push(page);
} else if found_tags {
break;
}
}
}
}
if pages.is_empty() {
return Ok(tags);
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 7 {
return Ok(tags);
}
let comment_data = &packets[0][7..];
let mut cursor = Cursor::new(comment_data);
let _ = tags
.inner
.load(&mut cursor, crate::vorbis::ErrorMode::Replace, false);
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: &TheoraTags) -> 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() >= 7
&& first_packet[0] == 0x81
&& &first_packet[1..7] == b"theora"
{
comment_pages.push(page);
found_tags = true;
} else if found_tags && !comment_pages.last().is_none_or(|p| p.is_complete()) {
comment_pages.push(page);
} else if found_tags {
break;
}
}
}
}
if comment_pages.is_empty() {
return Err(AudexError::InvalidData(
"No Theora comment packet found".to_string(),
));
}
let old_packets = OggPage::to_packets(&comment_pages, false)?;
if old_packets.is_empty() {
return Err(AudexError::InvalidData(
"Failed to reconstruct comment packet".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 vcomment_data = {
let mut data = b"\x81theora".to_vec();
let mut vcomment_bytes = Vec::new();
let mut comment_to_write = tags.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(false))?;
data.extend_from_slice(&vcomment_bytes);
data
};
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(None::<fn(&crate::tags::PaddingInfo) -> i64>);
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.clone(), &comment_pages);
let new_pages = if new_pages.is_empty() {
let first_sequence = comment_pages[0].sequence;
let original_granule = comment_pages
.last()
.map(|p| {
if p.position < 0 {
0u64
} else {
p.position as u64
}
})
.unwrap_or(0);
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, 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 = TheoraTags {
inner,
serial: self.info.serial,
};
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 theora = Self::load_async(path).await?;
theora.clear_async().await
}
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
OggTheora::delete_async(path).await
}