use async_trait::async_trait;
use oximedia_core::{OxiError, OxiResult};
use oximedia_io::MediaSource;
use std::io::SeekFrom;
use super::tags::TagMap;
use super::util::MediaSourceExt;
use super::vorbis::VorbisComments;
use crate::demux::flac::metadata::{BlockType, MetadataBlock};
#[async_trait]
pub trait MetadataWriter: Sized {
async fn write<R: MediaSource>(source: &mut R, tags: &TagMap) -> OxiResult<()>;
}
pub struct FlacMetadataWriter;
#[async_trait]
impl MetadataWriter for FlacMetadataWriter {
async fn write<R: MediaSource>(source: &mut R, tags: &TagMap) -> OxiResult<()> {
source.seek(SeekFrom::Start(0)).await?;
let mut magic = [0u8; 4];
source.read_exact(&mut magic).await?;
if &magic != b"fLaC" {
return Err(OxiError::UnknownFormat);
}
let mut blocks = Vec::new();
let mut vorbis_comment_found = false;
let _stream_info_block: Option<MetadataBlock> = None;
loop {
let _start_pos = MediaSource::position(source);
let mut header = [0u8; 4];
if source.read_exact(&mut header).await.is_err() {
break;
}
let is_last = header[0] & 0x80 != 0;
let block_type = BlockType::from(header[0]);
let length = u32::from_be_bytes([0, header[1], header[2], header[3]]);
let mut block_data = vec![0u8; length as usize];
source.read_exact(&mut block_data).await?;
let block = MetadataBlock {
is_last,
block_type,
length,
data: block_data,
};
#[allow(clippy::match_single_binding)]
#[allow(unused_variables)]
if block_type == BlockType::StreamInfo {
let _stream_info = block.clone();
}
if block_type == BlockType::VorbisComment {
vorbis_comment_found = true;
let new_comments = Self::create_vorbis_comment_block(tags, is_last);
blocks.push(new_comments);
} else {
blocks.push(block);
}
if is_last {
break;
}
}
if !vorbis_comment_found {
let new_comments = Self::create_vorbis_comment_block(tags, false);
blocks.push(new_comments);
}
if let Some(last_block) = blocks.last_mut() {
last_block.is_last = true;
}
for block in blocks.iter_mut().rev().skip(1) {
block.is_last = false;
}
Ok(())
}
}
impl FlacMetadataWriter {
fn create_vorbis_comment_block(tags: &TagMap, is_last: bool) -> MetadataBlock {
let mut comments = VorbisComments::with_vendor("OxiMedia");
comments.tags = tags.clone();
let data = comments.encode();
#[allow(clippy::cast_possible_truncation)]
let length = data.len() as u32;
MetadataBlock {
is_last,
block_type: BlockType::VorbisComment,
length,
data,
}
}
}
pub struct MatroskaMetadataWriter;
#[async_trait]
impl MetadataWriter for MatroskaMetadataWriter {
async fn write<R: MediaSource>(_source: &mut R, _tags: &TagMap) -> OxiResult<()> {
Err(OxiError::Unsupported(
"Matroska metadata writing not yet implemented".into(),
))
}
}
pub struct OggMetadataWriter;
#[async_trait]
impl MetadataWriter for OggMetadataWriter {
async fn write<R: MediaSource>(_source: &mut R, _tags: &TagMap) -> OxiResult<()> {
Err(OxiError::Unsupported(
"Ogg metadata writing not yet implemented".into(),
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_flac_create_vorbis_comment_block() {
let mut tags = TagMap::new();
tags.set("TITLE", "Test Title");
tags.set("ARTIST", "Test Artist");
let block = FlacMetadataWriter::create_vorbis_comment_block(&tags, false);
assert_eq!(block.block_type, BlockType::VorbisComment);
assert!(!block.is_last);
assert!(block.length > 0);
let comments = VorbisComments::parse(&block.data).expect("operation should succeed");
assert_eq!(comments.tags.get_text("TITLE"), Some("Test Title"));
assert_eq!(comments.tags.get_text("ARTIST"), Some("Test Artist"));
}
#[test]
fn test_flac_create_empty_vorbis_comment_block() {
let tags = TagMap::new();
let block = FlacMetadataWriter::create_vorbis_comment_block(&tags, true);
assert_eq!(block.block_type, BlockType::VorbisComment);
assert!(block.is_last);
let comments = VorbisComments::parse(&block.data).expect("operation should succeed");
assert!(comments.is_empty());
}
}