use crate::config::WriteOptions;
use crate::error::Result;
use crate::id3::v2::{FrameFlags, FrameHeader, FrameId};
use crate::tag::TagType;
use crate::util::alloc::VecFallibleCapacity;
use crate::util::text::{TextDecodeOptions, TextEncoding, decode_text};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::io::Read;
use byteorder::ReadBytesExt;
const FRAME_ID: FrameId<'static> = FrameId::Valid(Cow::Borrowed("POPM"));
#[derive(Clone, Debug, Eq)]
pub struct PopularimeterFrame<'a> {
pub(crate) header: FrameHeader<'a>,
pub email: Cow<'a, str>,
pub rating: u8,
pub counter: u64,
}
impl PartialEq for PopularimeterFrame<'_> {
fn eq(&self, other: &Self) -> bool {
self.email == other.email
}
}
impl Hash for PopularimeterFrame<'_> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.email.hash(state);
}
}
impl<'a> PopularimeterFrame<'a> {
pub fn new(email: impl Into<Cow<'a, str>>, rating: u8, counter: u64) -> Self {
let header = FrameHeader::new(FRAME_ID, FrameFlags::default());
Self {
header,
email: email.into(),
rating,
counter,
}
}
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 parse<R>(reader: &mut R, frame_flags: FrameFlags) -> Result<Self>
where
R: Read,
{
let email = decode_text(
reader,
TextDecodeOptions::new()
.encoding(TextEncoding::Latin1)
.terminated(true),
)?;
let rating = reader.read_u8()?;
let mut counter_content = Vec::new();
reader.read_to_end(&mut counter_content)?;
let counter;
let remaining_size = counter_content.len();
if remaining_size > 8 {
counter = u64::MAX;
} else {
let mut counter_bytes = [0; 8];
let counter_start_pos = 8 - remaining_size;
counter_bytes[counter_start_pos..].copy_from_slice(&counter_content);
counter = u64::from_be_bytes(counter_bytes);
}
let header = FrameHeader::new(FRAME_ID, frame_flags);
Ok(Self {
header,
email: Cow::Owned(email.content),
rating,
counter,
})
}
pub fn as_bytes(&self, write_options: WriteOptions) -> Result<Vec<u8>> {
let mut content = Vec::try_with_capacity_stable(self.email.len() + 9)?;
content.extend(TextEncoding::Latin1.encode(
&self.email,
true,
write_options.lossy_text_encoding,
)?);
content.push(self.rating);
if let Ok(counter) = u32::try_from(self.counter) {
content.extend(counter.to_be_bytes())
} else {
let counter_bytes = self.counter.to_be_bytes();
let i = counter_bytes.iter().position(|b| *b != 0).unwrap_or(4);
content.extend(&counter_bytes[i..]);
}
Ok(content)
}
}
impl PopularimeterFrame<'static> {
pub(crate) fn downgrade(&self) -> PopularimeterFrame<'_> {
PopularimeterFrame {
header: self.header.downgrade(),
email: Cow::Borrowed(&self.email),
rating: self.rating,
counter: self.counter,
}
}
}
impl<'a> From<crate::tag::items::popularimeter::Popularimeter<'a>> for PopularimeterFrame<'a> {
fn from(item: crate::tag::items::popularimeter::Popularimeter<'a>) -> Self {
let rating = item.mapped_value(TagType::Id3v2);
Self {
header: FrameHeader::new(FRAME_ID, FrameFlags::default()),
email: item.email.unwrap_or(Cow::Borrowed("")),
rating,
counter: item.play_counter,
}
}
}
#[cfg(test)]
mod tests {
use crate::config::WriteOptions;
use crate::id3::v2::items::popularimeter::PopularimeterFrame;
fn test_popm(popm: &PopularimeterFrame<'_>) {
let email = popm.email.clone();
let rating = popm.rating;
let counter = popm.counter;
let popm_bytes = popm.as_bytes(WriteOptions::default()).unwrap();
assert_eq!(&popm_bytes[..email.len()], email.as_bytes());
assert_eq!(popm_bytes[email.len()], 0);
assert_eq!(popm_bytes[email.len() + 1], rating);
let counter_len = if u32::try_from(counter).is_ok() {
4
} else {
let counter_bytes = counter.to_be_bytes();
let i = counter_bytes.iter().position(|b| *b != 0).unwrap_or(4);
counter_bytes.len() - i
};
assert_eq!(popm_bytes[email.len() + 2..].len(), counter_len);
}
#[test_log::test]
fn write_popm() {
let popm_u32_boundary = PopularimeterFrame::new("foo@bar.com", 255, u64::from(u32::MAX));
test_popm(&popm_u32_boundary);
let popm_u40 = PopularimeterFrame::new("baz@qux.com", 196, u64::from(u32::MAX) + 1);
test_popm(&popm_u40);
}
}