use std::collections::BTreeMap;
use id3::frame::{Comment, ExtendedText, PictureType};
use id3::{Tag, TagLike, Version};
use crate::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FrameId([u8; 4]);
impl FrameId {
pub const TITLE: Self = Self(*b"TIT2");
pub const GROUPING: Self = Self(*b"TIT1");
pub const ARTIST: Self = Self(*b"TPE1");
pub const ALBUM_ARTIST: Self = Self(*b"TPE2");
pub const ALBUM: Self = Self(*b"TALB");
pub const COMPOSER: Self = Self(*b"TCOM");
pub const TRACK: Self = Self(*b"TRCK");
pub const DISC: Self = Self(*b"TPOS");
pub const GENRE: Self = Self(*b"TCON");
pub const RECORDING_DATE: Self = Self(*b"TDRC");
pub const CONDUCTOR: Self = Self(*b"TPE3");
pub const PUBLISHER: Self = Self(*b"TPUB");
pub const COPYRIGHT: Self = Self(*b"TCOP");
pub const ISRC: Self = Self(*b"TSRC");
pub fn new(id: &str) -> Result<Self> {
let bytes: [u8; 4] = id
.as_bytes()
.try_into()
.map_err(|_| Error::InvalidFrameId(id.to_owned()))?;
let valid = bytes[0] == b'T'
&& bytes != *b"TXXX"
&& bytes
.iter()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit());
if valid {
Ok(Self(bytes))
} else {
Err(Error::InvalidFrameId(id.to_owned()))
}
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.0).unwrap_or_default()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum Change {
#[default]
Keep,
Set(String),
Clear,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Picture {
pub mime_type: String,
pub data: Vec<u8>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
enum PictureChange {
#[default]
Keep,
Set(Picture),
Clear,
}
const COMMENT_LANGUAGE: &str = "eng";
#[derive(Debug, Clone, Default)]
pub struct TagEdit {
frames: BTreeMap<FrameId, Change>,
user_text: BTreeMap<String, Change>,
comment: Change,
picture: PictureChange,
}
impl TagEdit {
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set(mut self, id: FrameId, value: impl Into<String>) -> Self {
let value = value.into();
let change = if value.trim().is_empty() {
Change::Clear
} else {
Change::Set(value)
};
self.frames.insert(id, change);
self
}
#[must_use]
pub fn clear(mut self, id: FrameId) -> Self {
self.frames.insert(id, Change::Clear);
self
}
#[must_use]
pub fn set_user_text(
mut self,
description: impl Into<String>,
value: impl Into<String>,
) -> Self {
let value = value.into();
let change = if value.trim().is_empty() {
Change::Clear
} else {
Change::Set(value)
};
self.user_text.insert(description.into(), change);
self
}
#[must_use]
pub fn clear_user_text(mut self, description: impl Into<String>) -> Self {
self.user_text.insert(description.into(), Change::Clear);
self
}
#[must_use]
pub fn set_comment(mut self, text: impl Into<String>) -> Self {
let text = text.into();
self.comment = if text.trim().is_empty() {
Change::Clear
} else {
Change::Set(text)
};
self
}
#[must_use]
pub fn set_cover(mut self, picture: Picture) -> Self {
self.picture = PictureChange::Set(picture);
self
}
#[must_use]
pub fn clear_cover(mut self) -> Self {
self.picture = PictureChange::Clear;
self
}
pub fn is_noop(&self) -> bool {
self.frames.values().all(|c| *c == Change::Keep)
&& self.user_text.values().all(|c| *c == Change::Keep)
&& self.comment == Change::Keep
&& self.picture == PictureChange::Keep
}
pub(crate) fn apply(&self, tag: &mut Tag) {
for (id, change) in &self.frames {
match change {
Change::Keep => {}
Change::Set(text) => tag.set_text(id.as_str(), text.as_str()),
Change::Clear => {
tag.remove(id.as_str());
}
}
}
for (description, change) in &self.user_text {
if *change == Change::Keep {
continue;
}
tag.remove_extended_text(Some(description.as_str()), None);
if let Change::Set(value) = change {
tag.add_frame(ExtendedText {
description: description.clone(),
value: value.clone(),
});
}
}
match &self.comment {
Change::Keep => {}
change => {
tag.remove_comment(Some(""), None);
if let Change::Set(text) = change {
tag.add_frame(Comment {
lang: COMMENT_LANGUAGE.to_owned(),
description: String::new(),
text: text.clone(),
});
}
}
}
match &self.picture {
PictureChange::Keep => {}
change => {
tag.remove_picture_by_type(PictureType::CoverFront);
if let PictureChange::Set(picture) = change {
tag.add_frame(id3::frame::Picture {
mime_type: picture.mime_type.clone(),
picture_type: PictureType::CoverFront,
description: String::new(),
data: picture.data.clone(),
});
}
}
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct WriteOptions {
pub version: Option<Version>,
}
impl WriteOptions {
#[must_use]
pub fn with_version(mut self, version: Version) -> Self {
self.version = Some(version);
self
}
}