lofty/ape/tag/
mod.rs

1pub(crate) mod item;
2pub(crate) mod read;
3mod write;
4
5use crate::ape::tag::item::{ApeItem, ApeItemRef};
6use crate::config::WriteOptions;
7use crate::error::{LoftyError, Result};
8use crate::id3::v2::util::pairs::{NUMBER_PAIR_KEYS, format_number_pair, set_number};
9use crate::tag::item::ItemValueRef;
10use crate::tag::{
11	Accessor, ItemKey, ItemValue, MergeTag, SplitTag, Tag, TagExt, TagItem, TagType, try_parse_year,
12};
13use crate::util::flag_item;
14use crate::util::io::{FileLike, Truncate};
15
16use std::borrow::Cow;
17use std::io::Write;
18use std::ops::Deref;
19
20use lofty_attr::tag;
21
22macro_rules! impl_accessor {
23	($($name:ident => $($key:literal)|+;)+) => {
24		paste::paste! {
25			$(
26				fn $name(&self) -> Option<Cow<'_, str>> {
27					$(
28						if let Some(i) = self.get($key) {
29							if let ItemValue::Text(val) = i.value() {
30								return Some(Cow::Borrowed(val));
31							}
32						}
33					)+
34
35					None
36				}
37
38				fn [<set_ $name>](&mut self, value: String) {
39					self.insert(ApeItem {
40						read_only: false,
41						key: String::from(crate::tag::item::first_key!($($key)|*)),
42						value: ItemValue::Text(value)
43					})
44				}
45
46				fn [<remove_ $name>](&mut self) {
47					$(
48						self.remove($key);
49					)+
50				}
51			)+
52		}
53	}
54}
55
56/// ## Item storage
57///
58/// `APE` isn't a very strict format. An [`ApeItem`] only restricted by its name, meaning it can use
59/// a normal [`ItemValue`](crate::tag::ItemValue) unlike other formats.
60///
61/// Pictures are stored as [`ItemValue::Binary`](crate::tag::ItemValue::Binary), and can be converted with
62/// [`Picture::from_ape_bytes`](crate::picture::Picture::from_ape_bytes). For the appropriate item keys, see
63/// [`APE_PICTURE_TYPES`](crate::ape::APE_PICTURE_TYPES).
64///
65/// ## Conversions
66///
67/// ### To `Tag`
68///
69/// Any [`ApeItem`] with an [`ItemKey`] mapping will have a 1:1 conversion to [`TagItem`].
70///
71/// ### From `Tag`
72///
73/// When converting pictures, any of type [`PictureType::Undefined`](crate::picture::PictureType::Undefined) will be discarded.
74/// For items, see [`ApeItem::new`].
75#[derive(Default, Debug, PartialEq, Eq, Clone)]
76#[tag(
77	description = "An `APE` tag",
78	supported_formats(Ape, Mpeg, Mpc, WavPack)
79)]
80pub struct ApeTag {
81	/// Whether or not to mark the tag as read only
82	pub read_only: bool,
83	pub(super) items: Vec<ApeItem>,
84}
85
86impl ApeTag {
87	/// Create a new empty `ApeTag`
88	///
89	/// # Examples
90	///
91	/// ```rust
92	/// use lofty::ape::ApeTag;
93	/// use lofty::tag::TagExt;
94	///
95	/// let ape_tag = ApeTag::new();
96	/// assert!(ape_tag.is_empty());
97	/// ```
98	pub fn new() -> Self {
99		Self::default()
100	}
101
102	/// Get an [`ApeItem`] by key
103	///
104	/// NOTE: While `APE` items are supposed to be case-sensitive,
105	/// this rule is rarely followed, so this will ignore case when searching.
106	///
107	/// # Examples
108	///
109	/// ```rust
110	/// use lofty::ape::ApeTag;
111	/// use lofty::tag::Accessor;
112	///
113	/// let mut ape_tag = ApeTag::new();
114	/// ape_tag.set_title(String::from("Foo title"));
115	///
116	/// // Get the title by its key
117	/// let title = ape_tag.get("Title");
118	/// assert!(title.is_some());
119	/// ```
120	pub fn get(&self, key: &str) -> Option<&ApeItem> {
121		self.items
122			.iter()
123			.find(|i| i.key().eq_ignore_ascii_case(key))
124	}
125
126	/// Insert an [`ApeItem`]
127	///
128	/// This will remove any item with the same key prior to insertion
129	pub fn insert(&mut self, value: ApeItem) {
130		self.remove(value.key());
131		self.items.push(value);
132	}
133
134	/// Remove an [`ApeItem`] by key
135	///
136	/// NOTE: Like [`ApeTag::get`], this is not case-sensitive
137	///
138	/// # Examples
139	///
140	/// ```rust
141	/// use lofty::ape::ApeTag;
142	/// use lofty::tag::Accessor;
143	///
144	/// let mut ape_tag = ApeTag::new();
145	/// ape_tag.set_title(String::from("Foo title"));
146	///
147	/// // Get the title by its key
148	/// let title = ape_tag.get("Title");
149	/// assert!(title.is_some());
150	///
151	/// // Remove the title
152	/// ape_tag.remove("Title");
153	///
154	/// let title = ape_tag.get("Title");
155	/// assert!(title.is_none());
156	/// ```
157	pub fn remove(&mut self, key: &str) {
158		self.items.retain(|i| !i.key().eq_ignore_ascii_case(key));
159	}
160
161	fn insert_item(&mut self, item: TagItem) {
162		match item.key() {
163			ItemKey::TrackNumber => set_number(&item, |number| self.set_track(number)),
164			ItemKey::TrackTotal => set_number(&item, |number| self.set_track_total(number)),
165			ItemKey::DiscNumber => set_number(&item, |number| self.set_disk(number)),
166			ItemKey::DiscTotal => set_number(&item, |number| self.set_disk_total(number)),
167
168			// Normalize flag items
169			ItemKey::FlagCompilation => {
170				let Some(text) = item.item_value.text() else {
171					return;
172				};
173
174				let Some(flag) = flag_item(text) else {
175					return;
176				};
177
178				let value = u8::from(flag).to_string();
179				self.insert(ApeItem::text("Compilation", value));
180			},
181			_ => {
182				if let Ok(item) = item.try_into() {
183					self.insert(item);
184				}
185			},
186		}
187	}
188
189	fn split_num_pair(&self, key: &str) -> (Option<u32>, Option<u32>) {
190		if let Some(ApeItem {
191			value: ItemValue::Text(text),
192			..
193		}) = self.get(key)
194		{
195			let mut split = text.split('/').flat_map(str::parse::<u32>);
196			return (split.next(), split.next());
197		}
198
199		(None, None)
200	}
201
202	fn insert_number_pair(&mut self, key: &'static str, number: Option<u32>, total: Option<u32>) {
203		if let Some(value) = format_number_pair(number, total) {
204			self.insert(ApeItem::text(key, value));
205		} else {
206			log::warn!("{key} is not set. number: {number:?}, total: {total:?}");
207		}
208	}
209}
210
211impl IntoIterator for ApeTag {
212	type Item = ApeItem;
213	type IntoIter = std::vec::IntoIter<Self::Item>;
214
215	fn into_iter(self) -> Self::IntoIter {
216		self.items.into_iter()
217	}
218}
219
220impl<'a> IntoIterator for &'a ApeTag {
221	type Item = &'a ApeItem;
222	type IntoIter = std::slice::Iter<'a, ApeItem>;
223
224	fn into_iter(self) -> Self::IntoIter {
225		self.items.iter()
226	}
227}
228
229impl Accessor for ApeTag {
230	impl_accessor!(
231		artist  => "Artist";
232		title   => "Title";
233		album   => "Album";
234		genre   => "GENRE";
235		comment => "Comment";
236	);
237
238	fn track(&self) -> Option<u32> {
239		self.split_num_pair("Track").0
240	}
241
242	fn set_track(&mut self, value: u32) {
243		self.insert_number_pair("Track", Some(value), self.track_total());
244	}
245
246	fn remove_track(&mut self) {
247		self.remove("Track");
248	}
249
250	fn track_total(&self) -> Option<u32> {
251		self.split_num_pair("Track").1
252	}
253
254	fn set_track_total(&mut self, value: u32) {
255		self.insert_number_pair("Track", self.track(), Some(value));
256	}
257
258	fn remove_track_total(&mut self) {
259		let existing_track_number = self.track();
260		self.remove("Track");
261
262		if let Some(track) = existing_track_number {
263			self.insert(ApeItem::text("Track", track.to_string()));
264		}
265	}
266
267	fn disk(&self) -> Option<u32> {
268		self.split_num_pair("Disc").0
269	}
270
271	fn set_disk(&mut self, value: u32) {
272		self.insert_number_pair("Disc", Some(value), self.disk_total());
273	}
274
275	fn remove_disk(&mut self) {
276		self.remove("Disc");
277	}
278
279	fn disk_total(&self) -> Option<u32> {
280		self.split_num_pair("Disc").1
281	}
282
283	fn set_disk_total(&mut self, value: u32) {
284		self.insert_number_pair("Disc", self.disk(), Some(value));
285	}
286
287	fn remove_disk_total(&mut self) {
288		let existing_track_number = self.track();
289		self.remove("Disc");
290
291		if let Some(track) = existing_track_number {
292			self.insert(ApeItem::text("Disc", track.to_string()));
293		}
294	}
295
296	fn year(&self) -> Option<u32> {
297		if let Some(ApeItem {
298			value: ItemValue::Text(text),
299			..
300		}) = self.get("Year")
301		{
302			return try_parse_year(text);
303		}
304
305		None
306	}
307
308	fn set_year(&mut self, value: u32) {
309		self.insert(ApeItem::text("Year", value.to_string()));
310	}
311
312	fn remove_year(&mut self) {
313		self.remove("Year");
314	}
315}
316
317impl TagExt for ApeTag {
318	type Err = LoftyError;
319	type RefKey<'a> = &'a str;
320
321	#[inline]
322	fn tag_type(&self) -> TagType {
323		TagType::Ape
324	}
325
326	fn len(&self) -> usize {
327		self.items.len()
328	}
329
330	fn contains<'a>(&'a self, key: Self::RefKey<'a>) -> bool {
331		self.items.iter().any(|i| i.key().eq_ignore_ascii_case(key))
332	}
333
334	fn is_empty(&self) -> bool {
335		self.items.is_empty()
336	}
337
338	/// Write an `APE` tag to a file
339	///
340	/// # Errors
341	///
342	/// * Attempting to write the tag to a format that does not support it
343	/// * An existing tag has an invalid size
344	fn save_to<F>(
345		&self,
346		file: &mut F,
347		write_options: WriteOptions,
348	) -> std::result::Result<(), Self::Err>
349	where
350		F: FileLike,
351		LoftyError: From<<F as Truncate>::Error>,
352	{
353		ApeTagRef {
354			read_only: self.read_only,
355			items: self.items.iter().map(Into::into),
356		}
357		.write_to(file, write_options)
358	}
359
360	/// Dumps the tag to a writer
361	///
362	/// # Errors
363	///
364	/// * [`std::io::Error`]
365	fn dump_to<W: Write>(
366		&self,
367		writer: &mut W,
368		write_options: WriteOptions,
369	) -> std::result::Result<(), Self::Err> {
370		ApeTagRef {
371			read_only: self.read_only,
372			items: self.items.iter().map(Into::into),
373		}
374		.dump_to(writer, write_options)
375	}
376
377	fn clear(&mut self) {
378		self.items.clear();
379	}
380}
381
382#[derive(Debug, Clone, Default)]
383pub struct SplitTagRemainder(ApeTag);
384
385impl From<SplitTagRemainder> for ApeTag {
386	fn from(from: SplitTagRemainder) -> Self {
387		from.0
388	}
389}
390
391impl Deref for SplitTagRemainder {
392	type Target = ApeTag;
393
394	fn deref(&self) -> &Self::Target {
395		&self.0
396	}
397}
398
399impl SplitTag for ApeTag {
400	type Remainder = SplitTagRemainder;
401
402	fn split_tag(mut self) -> (Self::Remainder, Tag) {
403		fn split_pair(
404			content: &str,
405			tag: &mut Tag,
406			current_key: ItemKey,
407			total_key: ItemKey,
408		) -> Option<()> {
409			let mut split = content.splitn(2, '/');
410			let current = split.next()?.to_string();
411			tag.items
412				.push(TagItem::new(current_key, ItemValue::Text(current)));
413
414			if let Some(total) = split.next() {
415				tag.items
416					.push(TagItem::new(total_key, ItemValue::Text(total.to_string())))
417			}
418
419			Some(())
420		}
421
422		let mut tag = Tag::new(TagType::Ape);
423
424		for item in std::mem::take(&mut self.items) {
425			let item_key = ItemKey::from_key(TagType::Ape, item.key());
426
427			// The text pairs need some special treatment
428			match (item_key, item.value()) {
429				(ItemKey::TrackNumber | ItemKey::TrackTotal, ItemValue::Text(val))
430					if split_pair(val, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
431						.is_some() =>
432				{
433					continue; // Item consumed
434				},
435				(ItemKey::DiscNumber | ItemKey::DiscTotal, ItemValue::Text(val))
436					if split_pair(val, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
437						.is_some() =>
438				{
439					continue; // Item consumed
440				},
441				(ItemKey::MovementNumber | ItemKey::MovementTotal, ItemValue::Text(val))
442					if split_pair(
443						val,
444						&mut tag,
445						ItemKey::MovementNumber,
446						ItemKey::MovementTotal,
447					)
448					.is_some() =>
449				{
450					continue; // Item consumed
451				},
452				(k, _) => {
453					tag.items.push(TagItem::new(k, item.value));
454				},
455			}
456		}
457
458		(SplitTagRemainder(self), tag)
459	}
460}
461
462impl MergeTag for SplitTagRemainder {
463	type Merged = ApeTag;
464
465	fn merge_tag(self, tag: Tag) -> Self::Merged {
466		let Self(mut merged) = self;
467
468		for item in tag.items {
469			merged.insert_item(item);
470		}
471
472		for pic in tag.pictures {
473			if let Some(key) = pic.pic_type.as_ape_key() {
474				if let Ok(item) =
475					ApeItem::new(key.to_string(), ItemValue::Binary(pic.as_ape_bytes()))
476				{
477					merged.insert(item)
478				}
479			}
480		}
481
482		merged
483	}
484}
485
486impl From<ApeTag> for Tag {
487	fn from(input: ApeTag) -> Self {
488		input.split_tag().1
489	}
490}
491
492impl From<Tag> for ApeTag {
493	fn from(input: Tag) -> Self {
494		SplitTagRemainder::default().merge_tag(input)
495	}
496}
497
498pub(crate) struct ApeTagRef<'a, I>
499where
500	I: Iterator<Item = ApeItemRef<'a>>,
501{
502	pub(crate) read_only: bool,
503	pub(crate) items: I,
504}
505
506impl<'a, I> ApeTagRef<'a, I>
507where
508	I: Iterator<Item = ApeItemRef<'a>>,
509{
510	pub(crate) fn write_to<F>(&mut self, file: &mut F, write_options: WriteOptions) -> Result<()>
511	where
512		F: FileLike,
513		LoftyError: From<<F as Truncate>::Error>,
514	{
515		write::write_to(file, self, write_options)
516	}
517
518	pub(crate) fn dump_to<W: Write>(
519		&mut self,
520		writer: &mut W,
521		write_options: WriteOptions,
522	) -> Result<()> {
523		let temp = write::create_ape_tag(self, std::iter::empty(), write_options)?;
524		writer.write_all(&temp)?;
525
526		Ok(())
527	}
528}
529
530pub(crate) fn tagitems_into_ape(tag: &Tag) -> impl Iterator<Item = ApeItemRef<'_>> {
531	fn create_apeitemref_for_number_pair<'a>(
532		number: Option<&str>,
533		total: Option<&str>,
534		key: &'a str,
535	) -> Option<ApeItemRef<'a>> {
536		format_number_pair(number, total).map(|value| ApeItemRef {
537			read_only: false,
538			key,
539			value: ItemValueRef::Text(Cow::Owned(value)),
540		})
541	}
542
543	tag.items()
544		.filter(|item| !NUMBER_PAIR_KEYS.contains(item.key()))
545		.filter_map(|i| {
546			i.key().map_key(TagType::Ape, true).map(|key| ApeItemRef {
547				read_only: false,
548				key,
549				value: (&i.item_value).into(),
550			})
551		})
552		.chain(create_apeitemref_for_number_pair(
553			tag.get_string(&ItemKey::TrackNumber),
554			tag.get_string(&ItemKey::TrackTotal),
555			"Track",
556		))
557		.chain(create_apeitemref_for_number_pair(
558			tag.get_string(&ItemKey::DiscNumber),
559			tag.get_string(&ItemKey::DiscTotal),
560			"Disk",
561		))
562}
563
564#[cfg(test)]
565mod tests {
566	use crate::ape::{ApeItem, ApeTag};
567	use crate::config::{ParseOptions, WriteOptions};
568	use crate::id3::v2::util::pairs::DEFAULT_NUMBER_IN_PAIR;
569	use crate::prelude::*;
570	use crate::tag::{ItemValue, Tag, TagItem, TagType};
571
572	use crate::picture::{MimeType, Picture, PictureType};
573	use std::io::Cursor;
574
575	#[test_log::test]
576	fn parse_ape() {
577		let mut expected_tag = ApeTag::default();
578
579		let title_item = ApeItem::new(
580			String::from("TITLE"),
581			ItemValue::Text(String::from("Foo title")),
582		)
583		.unwrap();
584
585		let artist_item = ApeItem::new(
586			String::from("ARTIST"),
587			ItemValue::Text(String::from("Bar artist")),
588		)
589		.unwrap();
590
591		let album_item = ApeItem::new(
592			String::from("ALBUM"),
593			ItemValue::Text(String::from("Baz album")),
594		)
595		.unwrap();
596
597		let comment_item = ApeItem::new(
598			String::from("COMMENT"),
599			ItemValue::Text(String::from("Qux comment")),
600		)
601		.unwrap();
602
603		let year_item =
604			ApeItem::new(String::from("YEAR"), ItemValue::Text(String::from("1984"))).unwrap();
605
606		let track_number_item =
607			ApeItem::new(String::from("TRACK"), ItemValue::Text(String::from("1"))).unwrap();
608
609		let genre_item = ApeItem::new(
610			String::from("GENRE"),
611			ItemValue::Text(String::from("Classical")),
612		)
613		.unwrap();
614
615		expected_tag.insert(title_item);
616		expected_tag.insert(artist_item);
617		expected_tag.insert(album_item);
618		expected_tag.insert(comment_item);
619		expected_tag.insert(year_item);
620		expected_tag.insert(track_number_item);
621		expected_tag.insert(genre_item);
622
623		let tag = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
624		let mut reader = Cursor::new(tag);
625
626		let (Some(parsed_tag), _) =
627			crate::ape::tag::read::read_ape_tag(&mut reader, false, ParseOptions::new()).unwrap()
628		else {
629			unreachable!();
630		};
631
632		assert_eq!(expected_tag.len(), parsed_tag.len());
633
634		for item in &expected_tag.items {
635			assert!(parsed_tag.items.contains(item));
636		}
637	}
638
639	#[test_log::test]
640	fn ape_re_read() {
641		let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
642		let mut reader = Cursor::new(tag_bytes);
643
644		let (Some(parsed_tag), _) =
645			crate::ape::tag::read::read_ape_tag(&mut reader, false, ParseOptions::new()).unwrap()
646		else {
647			unreachable!();
648		};
649
650		let mut writer = Vec::new();
651		parsed_tag
652			.dump_to(&mut writer, WriteOptions::default())
653			.unwrap();
654
655		let mut temp_reader = Cursor::new(writer);
656
657		let (Some(temp_parsed_tag), _) =
658			crate::ape::tag::read::read_ape_tag(&mut temp_reader, false, ParseOptions::new())
659				.unwrap()
660		else {
661			unreachable!();
662		};
663
664		assert_eq!(parsed_tag, temp_parsed_tag);
665	}
666
667	#[test_log::test]
668	fn ape_to_tag() {
669		let tag_bytes = crate::tag::utils::test_utils::read_path("tests/tags/assets/test.apev2");
670		let mut reader = Cursor::new(tag_bytes);
671
672		let (Some(ape), _) =
673			crate::ape::tag::read::read_ape_tag(&mut reader, false, ParseOptions::new()).unwrap()
674		else {
675			unreachable!();
676		};
677
678		let tag: Tag = ape.into();
679
680		crate::tag::utils::test_utils::verify_tag(&tag, true, true);
681	}
682
683	#[test_log::test]
684	fn tag_to_ape() {
685		fn verify_key(tag: &ApeTag, key: &str, expected_val: &str) {
686			assert_eq!(
687				tag.get(key).map(ApeItem::value),
688				Some(&ItemValue::Text(String::from(expected_val)))
689			);
690		}
691
692		let tag = crate::tag::utils::test_utils::create_tag(TagType::Ape);
693
694		let ape_tag: ApeTag = tag.into();
695
696		verify_key(&ape_tag, "Title", "Foo title");
697		verify_key(&ape_tag, "Artist", "Bar artist");
698		verify_key(&ape_tag, "Album", "Baz album");
699		verify_key(&ape_tag, "Comment", "Qux comment");
700		verify_key(&ape_tag, "Track", "1");
701		verify_key(&ape_tag, "Genre", "Classical");
702	}
703
704	#[test_log::test]
705	fn set_track() {
706		let mut ape = ApeTag::default();
707		let track = 1;
708
709		ape.set_track(track);
710
711		assert_eq!(ape.track().unwrap(), track);
712		assert!(ape.track_total().is_none());
713	}
714
715	#[test_log::test]
716	fn set_track_total() {
717		let mut ape = ApeTag::default();
718		let track_total = 2;
719
720		ape.set_track_total(track_total);
721
722		assert_eq!(ape.track().unwrap(), DEFAULT_NUMBER_IN_PAIR);
723		assert_eq!(ape.track_total().unwrap(), track_total);
724	}
725
726	#[test_log::test]
727	fn set_track_and_track_total() {
728		let mut ape = ApeTag::default();
729		let track = 1;
730		let track_total = 2;
731
732		ape.set_track(track);
733		ape.set_track_total(track_total);
734
735		assert_eq!(ape.track().unwrap(), track);
736		assert_eq!(ape.track_total().unwrap(), track_total);
737	}
738
739	#[test_log::test]
740	fn set_track_total_and_track() {
741		let mut ape = ApeTag::default();
742		let track_total = 2;
743		let track = 1;
744
745		ape.set_track_total(track_total);
746		ape.set_track(track);
747
748		assert_eq!(ape.track_total().unwrap(), track_total);
749		assert_eq!(ape.track().unwrap(), track);
750	}
751
752	#[test_log::test]
753	fn set_disk() {
754		let mut ape = ApeTag::default();
755		let disk = 1;
756
757		ape.set_disk(disk);
758
759		assert_eq!(ape.disk().unwrap(), disk);
760		assert!(ape.disk_total().is_none());
761	}
762
763	#[test_log::test]
764	fn set_disk_total() {
765		let mut ape = ApeTag::default();
766		let disk_total = 2;
767
768		ape.set_disk_total(disk_total);
769
770		assert_eq!(ape.disk().unwrap(), DEFAULT_NUMBER_IN_PAIR);
771		assert_eq!(ape.disk_total().unwrap(), disk_total);
772	}
773
774	#[test_log::test]
775	fn set_disk_and_disk_total() {
776		let mut ape = ApeTag::default();
777		let disk = 1;
778		let disk_total = 2;
779
780		ape.set_disk(disk);
781		ape.set_disk_total(disk_total);
782
783		assert_eq!(ape.disk().unwrap(), disk);
784		assert_eq!(ape.disk_total().unwrap(), disk_total);
785	}
786
787	#[test_log::test]
788	fn set_disk_total_and_disk() {
789		let mut ape = ApeTag::default();
790		let disk_total = 2;
791		let disk = 1;
792
793		ape.set_disk_total(disk_total);
794		ape.set_disk(disk);
795
796		assert_eq!(ape.disk_total().unwrap(), disk_total);
797		assert_eq!(ape.disk().unwrap(), disk);
798	}
799
800	#[test_log::test]
801	fn track_number_tag_to_ape() {
802		let track_number = 1;
803
804		let mut tag = Tag::new(TagType::Ape);
805
806		tag.push(TagItem::new(
807			ItemKey::TrackNumber,
808			ItemValue::Text(track_number.to_string()),
809		));
810
811		let tag: ApeTag = tag.into();
812
813		assert_eq!(tag.track().unwrap(), track_number);
814		assert!(tag.track_total().is_none());
815	}
816
817	#[test_log::test]
818	fn track_total_tag_to_ape() {
819		let track_total = 2;
820
821		let mut tag = Tag::new(TagType::Ape);
822
823		tag.push(TagItem::new(
824			ItemKey::TrackTotal,
825			ItemValue::Text(track_total.to_string()),
826		));
827
828		let tag: ApeTag = tag.into();
829
830		assert_eq!(tag.track().unwrap(), DEFAULT_NUMBER_IN_PAIR);
831		assert_eq!(tag.track_total().unwrap(), track_total);
832	}
833
834	#[test_log::test]
835	fn track_number_and_track_total_tag_to_ape() {
836		let track_number = 1;
837		let track_total = 2;
838
839		let mut tag = Tag::new(TagType::Ape);
840
841		tag.push(TagItem::new(
842			ItemKey::TrackNumber,
843			ItemValue::Text(track_number.to_string()),
844		));
845
846		tag.push(TagItem::new(
847			ItemKey::TrackTotal,
848			ItemValue::Text(track_total.to_string()),
849		));
850
851		let tag: ApeTag = tag.into();
852
853		assert_eq!(tag.track().unwrap(), track_number);
854		assert_eq!(tag.track_total().unwrap(), track_total);
855	}
856
857	#[test_log::test]
858	fn disk_number_tag_to_ape() {
859		let disk_number = 1;
860
861		let mut tag = Tag::new(TagType::Ape);
862
863		tag.push(TagItem::new(
864			ItemKey::DiscNumber,
865			ItemValue::Text(disk_number.to_string()),
866		));
867
868		let tag: ApeTag = tag.into();
869
870		assert_eq!(tag.disk().unwrap(), disk_number);
871		assert!(tag.disk_total().is_none());
872	}
873
874	#[test_log::test]
875	fn disk_total_tag_to_ape() {
876		let disk_total = 2;
877
878		let mut tag = Tag::new(TagType::Ape);
879
880		tag.push(TagItem::new(
881			ItemKey::DiscTotal,
882			ItemValue::Text(disk_total.to_string()),
883		));
884
885		let tag: ApeTag = tag.into();
886
887		assert_eq!(tag.disk().unwrap(), DEFAULT_NUMBER_IN_PAIR);
888		assert_eq!(tag.disk_total().unwrap(), disk_total);
889	}
890
891	#[test_log::test]
892	fn disk_number_and_disk_total_tag_to_ape() {
893		let disk_number = 1;
894		let disk_total = 2;
895
896		let mut tag = Tag::new(TagType::Ape);
897
898		tag.push(TagItem::new(
899			ItemKey::DiscNumber,
900			ItemValue::Text(disk_number.to_string()),
901		));
902
903		tag.push(TagItem::new(
904			ItemKey::DiscTotal,
905			ItemValue::Text(disk_total.to_string()),
906		));
907
908		let tag: ApeTag = tag.into();
909
910		assert_eq!(tag.disk().unwrap(), disk_number);
911		assert_eq!(tag.disk_total().unwrap(), disk_total);
912	}
913
914	#[test_log::test]
915	fn skip_reading_cover_art() {
916		let p = Picture::new_unchecked(
917			PictureType::CoverFront,
918			Some(MimeType::Jpeg),
919			None,
920			std::iter::repeat(0).take(50).collect::<Vec<u8>>(),
921		);
922
923		let mut tag = Tag::new(TagType::Ape);
924		tag.push_picture(p);
925
926		tag.set_artist(String::from("Foo artist"));
927
928		let mut writer = Vec::new();
929		tag.dump_to(&mut writer, WriteOptions::new()).unwrap();
930
931		let mut reader = Cursor::new(writer);
932		let (Some(ape), _) = crate::ape::tag::read::read_ape_tag(
933			&mut reader,
934			false,
935			ParseOptions::new().read_cover_art(false),
936		)
937		.unwrap() else {
938			unreachable!()
939		};
940
941		assert_eq!(ape.len(), 1);
942	}
943}