1use std::collections::BTreeMap;
2
3use id3::frame::{Comment, ExtendedText, PictureType};
4use id3::{Tag, TagLike, Version};
5
6use crate::{Error, Result};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub struct FrameId([u8; 4]);
11
12impl FrameId {
13 pub const TITLE: Self = Self(*b"TIT2");
15 pub const GROUPING: Self = Self(*b"TIT1");
17 pub const ARTIST: Self = Self(*b"TPE1");
19 pub const ALBUM_ARTIST: Self = Self(*b"TPE2");
21 pub const ALBUM: Self = Self(*b"TALB");
23 pub const COMPOSER: Self = Self(*b"TCOM");
25 pub const TRACK: Self = Self(*b"TRCK");
27 pub const DISC: Self = Self(*b"TPOS");
29 pub const GENRE: Self = Self(*b"TCON");
31 pub const RECORDING_DATE: Self = Self(*b"TDRC");
33 pub const CONDUCTOR: Self = Self(*b"TPE3");
35 pub const PUBLISHER: Self = Self(*b"TPUB");
37 pub const COPYRIGHT: Self = Self(*b"TCOP");
39 pub const ISRC: Self = Self(*b"TSRC");
41
42 pub fn new(id: &str) -> Result<Self> {
44 let bytes: [u8; 4] = id
45 .as_bytes()
46 .try_into()
47 .map_err(|_| Error::InvalidFrameId(id.to_owned()))?;
48 let valid = bytes[0] == b'T'
49 && bytes != *b"TXXX"
50 && bytes
51 .iter()
52 .all(|c| c.is_ascii_uppercase() || c.is_ascii_digit());
53 if valid {
54 Ok(Self(bytes))
55 } else {
56 Err(Error::InvalidFrameId(id.to_owned()))
57 }
58 }
59
60 pub fn as_str(&self) -> &str {
62 std::str::from_utf8(&self.0).unwrap_or_default()
63 }
64}
65
66#[derive(Debug, Clone, Default, PartialEq, Eq)]
68pub enum Change {
69 #[default]
71 Keep,
72 Set(String),
74 Clear,
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Picture {
81 pub mime_type: String,
83 pub data: Vec<u8>,
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
89enum PictureChange {
90 #[default]
91 Keep,
92 Set(Picture),
93 Clear,
94}
95
96const COMMENT_LANGUAGE: &str = "eng";
98
99#[derive(Debug, Clone, Default)]
101pub struct TagEdit {
102 frames: BTreeMap<FrameId, Change>,
103 user_text: BTreeMap<String, Change>,
104 comment: Change,
105 picture: PictureChange,
106}
107
108impl TagEdit {
109 pub fn new() -> Self {
111 Self::default()
112 }
113
114 #[must_use]
116 pub fn set(mut self, id: FrameId, value: impl Into<String>) -> Self {
117 let value = value.into();
118 let change = if value.trim().is_empty() {
119 Change::Clear
120 } else {
121 Change::Set(value)
122 };
123 self.frames.insert(id, change);
124 self
125 }
126
127 #[must_use]
129 pub fn clear(mut self, id: FrameId) -> Self {
130 self.frames.insert(id, Change::Clear);
131 self
132 }
133
134 #[must_use]
137 pub fn set_user_text(
138 mut self,
139 description: impl Into<String>,
140 value: impl Into<String>,
141 ) -> Self {
142 let value = value.into();
143 let change = if value.trim().is_empty() {
144 Change::Clear
145 } else {
146 Change::Set(value)
147 };
148 self.user_text.insert(description.into(), change);
149 self
150 }
151
152 #[must_use]
154 pub fn clear_user_text(mut self, description: impl Into<String>) -> Self {
155 self.user_text.insert(description.into(), Change::Clear);
156 self
157 }
158
159 #[must_use]
161 pub fn set_comment(mut self, text: impl Into<String>) -> Self {
162 let text = text.into();
163 self.comment = if text.trim().is_empty() {
164 Change::Clear
165 } else {
166 Change::Set(text)
167 };
168 self
169 }
170
171 #[must_use]
173 pub fn set_cover(mut self, picture: Picture) -> Self {
174 self.picture = PictureChange::Set(picture);
175 self
176 }
177
178 #[must_use]
180 pub fn clear_cover(mut self) -> Self {
181 self.picture = PictureChange::Clear;
182 self
183 }
184
185 pub fn is_noop(&self) -> bool {
187 self.frames.values().all(|c| *c == Change::Keep)
188 && self.user_text.values().all(|c| *c == Change::Keep)
189 && self.comment == Change::Keep
190 && self.picture == PictureChange::Keep
191 }
192
193 pub(crate) fn apply(&self, tag: &mut Tag) {
194 for (id, change) in &self.frames {
195 match change {
196 Change::Keep => {}
197 Change::Set(text) => tag.set_text(id.as_str(), text.as_str()),
198 Change::Clear => {
199 tag.remove(id.as_str());
200 }
201 }
202 }
203 for (description, change) in &self.user_text {
204 if *change == Change::Keep {
205 continue;
206 }
207 tag.remove_extended_text(Some(description.as_str()), None);
208 if let Change::Set(value) = change {
209 tag.add_frame(ExtendedText {
210 description: description.clone(),
211 value: value.clone(),
212 });
213 }
214 }
215 match &self.comment {
216 Change::Keep => {}
217 change => {
218 tag.remove_comment(Some(""), None);
219 if let Change::Set(text) = change {
220 tag.add_frame(Comment {
221 lang: COMMENT_LANGUAGE.to_owned(),
222 description: String::new(),
223 text: text.clone(),
224 });
225 }
226 }
227 }
228 match &self.picture {
229 PictureChange::Keep => {}
230 change => {
231 tag.remove_picture_by_type(PictureType::CoverFront);
232 if let PictureChange::Set(picture) = change {
233 tag.add_frame(id3::frame::Picture {
234 mime_type: picture.mime_type.clone(),
235 picture_type: PictureType::CoverFront,
236 description: String::new(),
237 data: picture.data.clone(),
238 });
239 }
240 }
241 }
242 }
243}
244
245#[derive(Debug, Clone, Default)]
247#[non_exhaustive]
248pub struct WriteOptions {
249 pub version: Option<Version>,
252}
253
254impl WriteOptions {
255 #[must_use]
257 pub fn with_version(mut self, version: Version) -> Self {
258 self.version = Some(version);
259 self
260 }
261}