1use crate::config::WriteOptions;
2use crate::error::{LoftyError, Result};
3use crate::id3::v1::constants::GENRES;
4use crate::tag::{Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType};
5use crate::util::io::{FileLike, Length, Truncate};
6
7use std::borrow::Cow;
8use std::io::Write;
9use std::path::Path;
10
11use lofty_attr::tag;
12
13macro_rules! impl_accessor {
14 ($($name:ident,)+) => {
15 paste::paste! {
16 $(
17 fn $name(&self) -> Option<Cow<'_, str>> {
18 if let Some(item) = self.$name.as_deref() {
19 return Some(Cow::Borrowed(item));
20 }
21
22 None
23 }
24
25 fn [<set_ $name>](&mut self, value: String) {
26 self.$name = Some(value)
27 }
28
29 fn [<remove_ $name>](&mut self) {
30 self.$name = None
31 }
32 )+
33 }
34 }
35}
36
37#[derive(Default, Debug, PartialEq, Eq, Clone)]
72#[tag(
73 description = "An ID3v1 tag",
74 supported_formats(Aac, Ape, Mpeg, WavPack, read_only(Mpc))
75)]
76pub struct Id3v1Tag {
77 pub title: Option<String>,
79 pub artist: Option<String>,
81 pub album: Option<String>,
83 pub year: Option<String>,
85 pub comment: Option<String>,
93 pub track_number: Option<u8>,
102 pub genre: Option<u8>,
107}
108
109impl Id3v1Tag {
110 pub fn new() -> Self {
122 Self::default()
123 }
124}
125
126impl Accessor for Id3v1Tag {
127 impl_accessor!(title, artist, album,);
128
129 fn genre(&self) -> Option<Cow<'_, str>> {
130 if let Some(g) = self.genre {
131 let g = g as usize;
132
133 if g < GENRES.len() {
134 return Some(Cow::Borrowed(GENRES[g]));
135 }
136 }
137
138 None
139 }
140
141 fn set_genre(&mut self, genre: String) {
142 let g_str = genre.as_str();
143
144 for (i, g) in GENRES.iter().enumerate() {
145 if g.eq_ignore_ascii_case(g_str) {
146 self.genre = Some(i as u8);
147 break;
148 }
149 }
150 }
151
152 fn remove_genre(&mut self) {
153 self.genre = None
154 }
155
156 fn track(&self) -> Option<u32> {
157 self.track_number.map(u32::from)
158 }
159
160 fn set_track(&mut self, value: u32) {
161 self.track_number = Some(value as u8);
162 }
163
164 fn remove_track(&mut self) {
165 self.track_number = None;
166 }
167
168 fn comment(&self) -> Option<Cow<'_, str>> {
169 self.comment.as_deref().map(Cow::Borrowed)
170 }
171
172 fn set_comment(&mut self, value: String) {
173 let mut resized = String::with_capacity(28);
174 for c in value.chars() {
175 if resized.len() + c.len_utf8() > 28 {
176 break;
177 }
178
179 resized.push(c);
180 }
181
182 self.comment = Some(resized);
183 }
184
185 fn remove_comment(&mut self) {
186 self.comment = None;
187 }
188
189 fn year(&self) -> Option<u32> {
190 if let Some(ref year) = self.year {
191 if let Ok(y) = year.parse() {
192 return Some(y);
193 }
194 }
195
196 None
197 }
198
199 fn set_year(&mut self, value: u32) {
200 self.year = Some(value.to_string());
201 }
202
203 fn remove_year(&mut self) {
204 self.year = None;
205 }
206}
207
208impl TagExt for Id3v1Tag {
209 type Err = LoftyError;
210 type RefKey<'a> = &'a ItemKey;
211
212 #[inline]
213 fn tag_type(&self) -> TagType {
214 TagType::Id3v1
215 }
216
217 fn len(&self) -> usize {
218 usize::from(self.title.is_some())
219 + usize::from(self.artist.is_some())
220 + usize::from(self.album.is_some())
221 + usize::from(self.year.is_some())
222 + usize::from(self.comment.is_some())
223 + usize::from(self.track_number.is_some())
224 + usize::from(self.genre.is_some())
225 }
226
227 fn contains<'a>(&'a self, key: Self::RefKey<'a>) -> bool {
228 match key {
229 ItemKey::TrackTitle => self.title.is_some(),
230 ItemKey::AlbumTitle => self.album.is_some(),
231 ItemKey::TrackArtist => self.artist.is_some(),
232 ItemKey::TrackNumber => self.track_number.is_some(),
233 ItemKey::Year => self.year.is_some(),
234 ItemKey::Genre => self.genre.is_some(),
235 ItemKey::Comment => self.comment.is_some(),
236 _ => false,
237 }
238 }
239
240 fn is_empty(&self) -> bool {
241 self.title.is_none()
242 && self.artist.is_none()
243 && self.album.is_none()
244 && self.year.is_none()
245 && self.comment.is_none()
246 && self.track_number.is_none()
247 && self.genre.is_none()
248 }
249
250 fn save_to<F>(
251 &self,
252 file: &mut F,
253 write_options: WriteOptions,
254 ) -> std::result::Result<(), Self::Err>
255 where
256 F: FileLike,
257 LoftyError: From<<F as Truncate>::Error>,
258 LoftyError: From<<F as Length>::Error>,
259 {
260 Into::<Id3v1TagRef<'_>>::into(self).write_to(file, write_options)
261 }
262
263 fn dump_to<W: Write>(
269 &self,
270 writer: &mut W,
271 write_options: WriteOptions,
272 ) -> std::result::Result<(), Self::Err> {
273 Into::<Id3v1TagRef<'_>>::into(self).dump_to(writer, write_options)
274 }
275
276 fn remove_from_path<P: AsRef<Path>>(&self, path: P) -> std::result::Result<(), Self::Err> {
277 TagType::Id3v1.remove_from_path(path)
278 }
279
280 fn remove_from<F>(&self, file: &mut F) -> std::result::Result<(), Self::Err>
281 where
282 F: FileLike,
283 LoftyError: From<<F as Truncate>::Error>,
284 LoftyError: From<<F as Length>::Error>,
285 {
286 TagType::Id3v1.remove_from(file)
287 }
288
289 fn clear(&mut self) {
290 *self = Self::default();
291 }
292}
293
294#[derive(Debug, Clone, Default)]
295pub struct SplitTagRemainder;
296
297impl SplitTag for Id3v1Tag {
298 type Remainder = SplitTagRemainder;
299
300 fn split_tag(mut self) -> (Self::Remainder, Tag) {
301 let mut tag = Tag::new(TagType::Id3v1);
302
303 self.title
304 .take()
305 .map(|t| tag.insert_text(ItemKey::TrackTitle, t));
306 self.artist
307 .take()
308 .map(|a| tag.insert_text(ItemKey::TrackArtist, a));
309 self.album
310 .take()
311 .map(|a| tag.insert_text(ItemKey::AlbumTitle, a));
312 self.year.take().map(|y| tag.insert_text(ItemKey::Year, y));
313 self.comment
314 .take()
315 .map(|c| tag.insert_text(ItemKey::Comment, c));
316
317 if let Some(t) = self.track_number.take() {
318 tag.items.push(TagItem::new(
319 ItemKey::TrackNumber,
320 ItemValue::Text(t.to_string()),
321 ))
322 }
323
324 if let Some(genre_index) = self.genre.take() {
325 if let Some(genre) = GENRES.get(genre_index as usize) {
326 tag.insert_text(ItemKey::Genre, (*genre).to_string());
327 }
328 }
329
330 (SplitTagRemainder, tag)
331 }
332}
333
334impl MergeTag for SplitTagRemainder {
335 type Merged = Id3v1Tag;
336
337 fn merge_tag(self, tag: Tag) -> Self::Merged {
338 tag.into()
339 }
340}
341
342impl From<Id3v1Tag> for Tag {
343 fn from(input: Id3v1Tag) -> Self {
344 input.split_tag().1
345 }
346}
347
348impl From<Tag> for Id3v1Tag {
349 fn from(mut input: Tag) -> Self {
350 let title = input.take_strings(&ItemKey::TrackTitle).next();
351 let artist = input.take_strings(&ItemKey::TrackArtist).next();
352 let album = input.take_strings(&ItemKey::AlbumTitle).next();
353 let year = input.year().map(|y| y.to_string());
354 let comment = input.take_strings(&ItemKey::Comment).next();
355 Self {
356 title,
357 artist,
358 album,
359 year,
360 comment,
361 track_number: input
362 .get_string(&ItemKey::TrackNumber)
363 .map(|g| g.parse::<u8>().ok())
364 .and_then(|g| g),
365 genre: input
366 .get_string(&ItemKey::Genre)
367 .map(|g| {
368 GENRES
369 .iter()
370 .position(|v| v == &g)
371 .map_or_else(|| g.parse::<u8>().ok(), |p| Some(p as u8))
372 })
373 .and_then(|g| g),
374 }
375 }
376}
377
378pub(crate) struct Id3v1TagRef<'a> {
379 pub title: Option<&'a str>,
380 pub artist: Option<&'a str>,
381 pub album: Option<&'a str>,
382 pub year: Option<&'a str>,
383 pub comment: Option<&'a str>,
384 pub track_number: Option<u8>,
385 pub genre: Option<u8>,
386}
387
388impl<'a> Into<Id3v1TagRef<'a>> for &'a Id3v1Tag {
389 fn into(self) -> Id3v1TagRef<'a> {
390 Id3v1TagRef {
391 title: self.title.as_deref(),
392 artist: self.artist.as_deref(),
393 album: self.album.as_deref(),
394 year: self.year.as_deref(),
395 comment: self.comment.as_deref(),
396 track_number: self.track_number,
397 genre: self.genre,
398 }
399 }
400}
401
402impl<'a> Into<Id3v1TagRef<'a>> for &'a Tag {
403 fn into(self) -> Id3v1TagRef<'a> {
404 Id3v1TagRef {
405 title: self.get_string(&ItemKey::TrackTitle),
406 artist: self.get_string(&ItemKey::TrackArtist),
407 album: self.get_string(&ItemKey::AlbumTitle),
408 year: self.get_string(&ItemKey::Year),
409 comment: self.get_string(&ItemKey::Comment),
410 track_number: self
411 .get_string(&ItemKey::TrackNumber)
412 .map(|g| g.parse::<u8>().ok())
413 .and_then(|g| g),
414 genre: self
415 .get_string(&ItemKey::Genre)
416 .map(|g| {
417 GENRES
418 .iter()
419 .position(|v| v == &g)
420 .map_or_else(|| g.parse::<u8>().ok(), |p| Some(p as u8))
421 })
422 .and_then(|g| g),
423 }
424 }
425}
426
427impl Id3v1TagRef<'_> {
428 pub(super) fn is_empty(&self) -> bool {
429 self.title.is_none()
430 && self.artist.is_none()
431 && self.album.is_none()
432 && self.year.is_none()
433 && self.comment.is_none()
434 && self.track_number.is_none()
435 && self.genre.is_none()
436 }
437
438 pub(crate) fn write_to<F>(&self, file: &mut F, write_options: WriteOptions) -> Result<()>
439 where
440 F: FileLike,
441 LoftyError: From<<F as Truncate>::Error>,
442 LoftyError: From<<F as Length>::Error>,
443 {
444 super::write::write_id3v1(file, self, write_options)
445 }
446
447 pub(crate) fn dump_to<W: Write>(
448 &mut self,
449 writer: &mut W,
450 _write_options: WriteOptions,
451 ) -> Result<()> {
452 let temp = super::write::encode(self)?;
453 writer.write_all(&temp)?;
454
455 Ok(())
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use crate::config::WriteOptions;
462 use crate::id3::v1::Id3v1Tag;
463 use crate::prelude::*;
464 use crate::tag::{Tag, TagType};
465
466 #[test_log::test]
467 fn parse_id3v1() {
468 let expected_tag = Id3v1Tag {
469 title: Some(String::from("Foo title")),
470 artist: Some(String::from("Bar artist")),
471 album: Some(String::from("Baz album")),
472 year: Some(String::from("1984")),
473 comment: Some(String::from("Qux comment")),
474 track_number: Some(1),
475 genre: Some(32),
476 };
477
478 let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.id3v1");
479 let parsed_tag = crate::id3::v1::read::parse_id3v1(tag.try_into().unwrap());
480
481 assert_eq!(expected_tag, parsed_tag);
482 }
483
484 #[test_log::test]
485 fn id3v2_re_read() {
486 let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.id3v1");
487 let parsed_tag = crate::id3::v1::read::parse_id3v1(tag.try_into().unwrap());
488
489 let mut writer = Vec::new();
490 parsed_tag
491 .dump_to(&mut writer, WriteOptions::default())
492 .unwrap();
493
494 let temp_parsed_tag = crate::id3::v1::read::parse_id3v1(writer.try_into().unwrap());
495
496 assert_eq!(parsed_tag, temp_parsed_tag);
497 }
498
499 #[test_log::test]
500 fn id3v1_to_tag() {
501 let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.id3v1");
502 let id3v1 = crate::id3::v1::read::parse_id3v1(tag_bytes.try_into().unwrap());
503
504 let tag: Tag = id3v1.into();
505
506 crate::tag::utils::test_utils::verify_tag(&tag, true, true);
507 }
508
509 #[test_log::test]
510 fn tag_to_id3v1() {
511 let tag = crate::tag::utils::test_utils::create_tag(TagType::Id3v1);
512
513 let id3v1_tag: Id3v1Tag = tag.into();
514
515 assert_eq!(id3v1_tag.title.as_deref(), Some("Foo title"));
516 assert_eq!(id3v1_tag.artist.as_deref(), Some("Bar artist"));
517 assert_eq!(id3v1_tag.album.as_deref(), Some("Baz album"));
518 assert_eq!(id3v1_tag.comment.as_deref(), Some("Qux comment"));
519 assert_eq!(id3v1_tag.track_number, Some(1));
520 assert_eq!(id3v1_tag.genre, Some(32));
521 }
522}