use crate::config::{ParseOptions, WriteOptions};
use crate::id3::v2::error::{FrameEncodingError, FrameParseError};
use crate::id3::v2::frame::list::FrameList;
use crate::id3::v2::read::read_all_frames_into_list;
use crate::id3::v2::tag::TITLE_ID;
use crate::id3::v2::{Frame, FrameFlags, FrameHeader, FrameId, Id3v2TagFlags, Id3v2Version};
use crate::util::alloc::VecFallibleCapacity;
use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text};
use std::borrow::Cow;
use std::hash::Hash;
use std::io::{Cursor, Read, Write};
use std::ops::Range;
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("CHAP"));
#[derive(Clone, Debug, Eq)]
pub struct ChapterFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub id: Cow<'a, str>,
pub times: Range<u32>,
pub offsets: Range<u32>,
pub children: FrameList<'a>,
}
impl PartialEq for ChapterFrame<'_> {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Hash for ChapterFrame<'_> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl<'a> ChapterFrame<'a> {
pub fn new(
id: impl Into<Cow<'a, str>>,
times: Range<u32>,
offsets: Range<u32>,
children: FrameList<'a>,
) -> Self {
let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
Self {
header,
id: id.into(),
times,
offsets,
children,
}
}
pub fn id(&self) -> FrameId<'_> {
FRAME_ID
}
pub fn flags(&self) -> FrameFlags {
self.header.flags
}
pub fn set_flags(&mut self, flags: FrameFlags) {
self.header.flags = flags;
}
pub fn title(&self) -> Option<&str> {
self.children.get_text(&TITLE_ID)
}
pub fn subtitle(&self) -> Option<&str> {
self.children
.get_text(&FrameId::Valid(Cow::Borrowed("TIT3")))
}
pub fn as_bytes(
&self,
version: Id3v2Version,
write_options: WriteOptions,
) -> Result<Vec<u8>, FrameEncodingError> {
if version == Id3v2Version::V2 {
return Err(FrameEncodingError::unsupported_version(version));
}
let Self {
header: _,
id,
times,
offsets,
children,
} = self;
let mut content = Cursor::new(Vec::try_with_capacity_stable(id.len() + 16)?);
content.write_all(&TextEncoding::Latin1.encode(
id,
true,
write_options.lossy_text_encoding,
)?)?;
content.write_u32::<BigEndian>(times.start)?;
content.write_u32::<BigEndian>(times.end)?;
content.write_u32::<BigEndian>(offsets.start)?;
content.write_u32::<BigEndian>(offsets.end)?;
match version {
Id3v2Version::V4 => crate::id3::v2::write::frame::create_items(
&mut content,
&mut children.iter().map(Frame::borrow),
write_options,
)?,
Id3v2Version::V3 => crate::id3::v2::write::frame::create_items_v3(
&mut content,
&mut children.iter().map(Frame::borrow),
write_options,
)?,
Id3v2Version::V2 => unreachable!(),
}
Ok(content.into_inner())
}
}
impl ChapterFrame<'static> {
pub fn parse<R>(
reader: &mut R,
parse_options: ParseOptions,
version: Id3v2Version,
frame_flags: FrameFlags,
) -> Result<Self, FrameParseError>
where
R: Read,
{
if version == Id3v2Version::V2 {
return Err(FrameParseError::unsupported_version(version));
}
let id = decode_text(
reader,
TextDecodeOptions::new()
.encoding(TextEncoding::Latin1)
.terminated(true),
)?;
let start_time = reader.read_u32::<BigEndian>()?;
let end_time = reader.read_u32::<BigEndian>()?;
let start_offset = reader.read_u32::<BigEndian>()?;
let end_offset = reader.read_u32::<BigEndian>()?;
let children =
read_all_frames_into_list(reader, version, Id3v2TagFlags::default(), parse_options)?;
let header = FrameHeader::new(FRAME_ID, frame_flags);
Ok(ChapterFrame {
header,
id: Cow::Owned(id.content),
times: start_time..end_time,
offsets: start_offset..end_offset,
children,
})
}
}
impl ChapterFrame<'_> {
pub(crate) fn borrow(&self) -> ChapterFrame<'_> {
ChapterFrame {
header: self.header.borrow(),
id: Cow::Borrowed(&self.id),
times: self.times.clone(),
offsets: self.offsets.clone(),
children: self.children.borrow(),
}
}
}
#[cfg(test)]
mod tests {
use crate::TextEncoding;
use crate::config::{ParseOptions, WriteOptions};
use crate::id3::v2::{
ChapterFrame, Frame, FrameFlags, FrameId, FrameList, Id3v2Version, TextInformationFrame,
};
use std::borrow::Cow;
fn expected() -> ChapterFrame<'static> {
let mut children = FrameList::new();
children.push(Frame::Text(TextInformationFrame::new(
FrameId::Valid(Cow::Borrowed("TIT2")),
TextEncoding::Latin1,
"Chapter 1 - Lofty",
)));
ChapterFrame::new("CH1", 1000..4000, 80..256, children)
}
#[test_log::test]
fn chap_decode() {
let cont = crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.chap");
let parsed_chap = ChapterFrame::parse(
&mut &cont[..],
ParseOptions::default(),
Id3v2Version::V4,
FrameFlags::default(),
)
.unwrap();
let expected = expected();
let ChapterFrame {
header,
id,
times,
offsets,
children,
} = parsed_chap;
assert_eq!(header, expected.header);
assert_eq!(id, expected.id);
assert_eq!(times, expected.times);
assert_eq!(offsets, expected.offsets);
assert_eq!(children, expected.children);
}
#[test_log::test]
fn chap_encode() {
let encoded = expected()
.as_bytes(Id3v2Version::V4, WriteOptions::default())
.unwrap();
let expected_bytes =
crate::tag::utils::test_utils::read_path("tests/tags/assets/id3v2/test.chap");
assert_eq!(encoded, expected_bytes);
}
}