lofty/id3/v1/
tag.rs

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/// ID3v1 is a severely limited format, with each field
38/// being incredibly small in size. All fields have been
39/// commented with their maximum sizes and any other additional
40/// restrictions.
41///
42/// Attempting to write a field greater than the maximum size
43/// will **not** error, it will just be shrunk.
44///
45/// ## Conversions
46///
47/// ### To `Tag`
48///
49/// All fields can be translated to a `TagItem`:
50///
51/// * `title` -> [`ItemKey::TrackTitle`]
52/// * `artist` -> [`ItemKey::TrackArtist`]
53/// * `album` -> [`ItemKey::AlbumTitle`]
54/// * `year` -> [`ItemKey::Year`]
55/// * `comment` -> [`ItemKey::Comment`]
56/// * `track_number` -> [`ItemKey::TrackNumber`]
57/// * `genre` -> [`ItemKey::Genre`] (As long as the genre is a valid index into [`GENRES`])
58///
59///
60/// ### From `Tag`
61///
62/// All of the [`ItemKey`]s referenced in the conversion to [`Tag`] will be checked.
63///
64/// The values will be used as-is, with two exceptions:
65///
66/// * [`ItemKey::TrackNumber`] - Will only be used if the value can be parsed as a `u8`
67/// * [`ItemKey::Genre`] - Will only be used if:
68///
69/// 	[`GENRES`] contains the string **OR** The [`ItemValue`](crate::ItemValue) can be parsed into
70/// 	a `u8` ***and*** it is a valid index into [`GENRES`]
71#[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	/// Track title, 30 bytes max
78	pub title: Option<String>,
79	/// Track artist, 30 bytes max
80	pub artist: Option<String>,
81	/// Album title, 30 bytes max
82	pub album: Option<String>,
83	/// Release year, 4 bytes max
84	pub year: Option<String>,
85	/// A short comment
86	///
87	/// The number of bytes differs between versions, but not much.
88	/// A V1 tag may have been read, which limits this field to 30 bytes.
89	/// A V1.1 tag, however, only has 28 bytes available.
90	///
91	/// **Lofty** will *always* write a V1.1 tag.
92	pub comment: Option<String>,
93	/// The track number, 1 byte max
94	///
95	/// Issues:
96	///
97	/// * The track number **cannot** be 0. Many readers, including Lofty,
98	/// look for a null byte at the end of the comment to differentiate
99	/// between V1 and V1.1.
100	/// * A V1 tag may have been read, which does *not* have a track number.
101	pub track_number: Option<u8>,
102	/// The track's genre, 1 byte max
103	///
104	/// ID3v1 has a predefined set of genres, see [`GENRES`](crate::id3::v1::GENRES).
105	/// This byte should be an index to a genre.
106	pub genre: Option<u8>,
107}
108
109impl Id3v1Tag {
110	/// Create a new empty `ID3v1Tag`
111	///
112	/// # Examples
113	///
114	/// ```rust
115	/// use lofty::id3::v1::Id3v1Tag;
116	/// use lofty::tag::TagExt;
117	///
118	/// let id3v1_tag = Id3v1Tag::new();
119	/// assert!(id3v1_tag.is_empty());
120	/// ```
121	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	/// Dumps the tag to a writer
264	///
265	/// # Errors
266	///
267	/// * [`std::io::Error`]
268	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}