1use crate::StorageFile;
2use crate::chunk;
3use crate::frame::{
4 Chapter, Comment, EncapsulatedObject, ExtendedLink, ExtendedText, Frame, InvolvedPeopleList,
5 Lyrics, Picture, SynchronisedLyrics, TableOfContents, UniqueFileIdentifier,
6};
7use crate::storage::{Format, Storage, plain::PlainStorage};
8use crate::stream;
9use crate::taglike::TagLike;
10use crate::v1;
11use crate::{Error, ErrorKind};
12use std::fmt;
13use std::fs::{self, File};
14use std::io::{self, BufRead, BufReader, Write};
15use std::iter::{FromIterator, Iterator};
16use std::path::Path;
17
18#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
20pub enum Version {
21 Id3v22,
23 Id3v23,
25 #[default]
27 Id3v24,
28}
29
30impl Version {
31 pub fn minor(self) -> u8 {
40 match self {
41 Version::Id3v22 => 2,
42 Version::Id3v23 => 3,
43 Version::Id3v24 => 4,
44 }
45 }
46}
47
48impl fmt::Display for Version {
49 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Version::Id3v22 => write!(f, "ID3v2.2"),
52 Version::Id3v23 => write!(f, "ID3v2.3"),
53 Version::Id3v24 => write!(f, "ID3v2.4"),
54 }
55 }
56}
57
58#[derive(Clone, Debug, Default, Eq)]
60pub struct Tag {
61 frames: Vec<Frame>,
63 version: Version,
65}
66
67impl<'a> Tag {
68 pub fn new() -> Tag {
70 Tag::default()
71 }
72
73 pub fn with_version(version: Version) -> Tag {
75 Tag {
76 version,
77 ..Tag::default()
78 }
79 }
80
81 pub fn is_candidate(mut reader: impl io::Read + io::Seek) -> crate::Result<bool> {
89 let initial_position = reader.stream_position()?;
90 let is_candidate = match stream::tag::locate_id3v2(&mut reader) {
91 Ok(_) => true,
92 Err(Error {
93 kind: ErrorKind::NoTag,
94 ..
95 }) => false,
96 Err(err) => return Err(err),
97 };
98 reader.seek(io::SeekFrom::Start(initial_position))?;
99 Ok(is_candidate)
100 }
101
102 pub fn skip(mut reader: impl io::Read + io::Seek) -> crate::Result<bool> {
105 let initial_position = reader.stream_position()?;
106 let range = match stream::tag::locate_id3v2(&mut reader) {
107 Ok(v) => v,
108 Err(Error {
109 kind: ErrorKind::NoTag,
110 ..
111 }) => return Ok(false),
112 Err(err) => return Err(err),
113 };
114 reader.seek(io::SeekFrom::Start(initial_position + range.end))?;
115 Ok(true)
116 }
117
118 pub fn remove_from_path(path: impl AsRef<Path>) -> crate::Result<bool> {
122 let mut file = fs::OpenOptions::new()
123 .read(true)
124 .write(true)
125 .create(false)
126 .truncate(false)
127 .open(path)?;
128 Self::remove_from_file(&mut file)
129 }
130
131 pub fn remove_from_file(mut file: impl StorageFile) -> crate::Result<bool> {
135 let location = match stream::tag::locate_id3v2(&mut file) {
136 Ok(l) => l,
137 Err(Error {
138 kind: ErrorKind::NoTag,
139 ..
140 }) => return Ok(false),
141 Err(err) => return Err(err),
142 };
143 let mut storage = PlainStorage::new(file, location);
146 storage.writer()?.flush()?;
147 Ok(true)
148 }
149
150 #[deprecated(note = "use read_from2")]
152 pub fn read_from(reader: impl io::Read) -> crate::Result<Tag> {
153 stream::tag::decode(reader)
154 }
155
156 pub fn read_from2(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
163 let mut b = BufReader::new(reader);
164 let probe = b.fill_buf()?;
165
166 match Format::magic(probe) {
167 Some(Format::Header) | None => stream::tag::decode(b),
168 Some(Format::Aiff) => chunk::load_id3_chunk::<chunk::AiffFormat, _>(b),
169 Some(Format::Wav) => chunk::load_id3_chunk::<chunk::WavFormat, _>(b),
170 }
171 }
172
173 #[cfg(feature = "tokio")]
175 pub async fn async_read_from(
176 reader: impl tokio::io::AsyncRead + std::marker::Unpin,
177 ) -> crate::Result<Tag> {
178 stream::tag::async_decode(reader).await
179 }
180
181 pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
183 Tag::read_from2(File::open(path)?)
184 }
185
186 #[cfg(feature = "tokio")]
188 pub async fn async_read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
189 let file = tokio::io::BufReader::new(tokio::fs::File::open(path).await?);
190 stream::tag::async_decode(file).await
191 }
192
193 #[deprecated(note = "use read_from")]
195 pub fn read_from_aiff(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
196 chunk::load_id3_chunk::<chunk::AiffFormat, _>(reader)
197 }
198
199 #[deprecated(note = "use read_from_path")]
201 pub fn read_from_aiff_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
202 let mut file = BufReader::new(File::open(path)?);
203 chunk::load_id3_chunk::<chunk::AiffFormat, _>(&mut file)
204 }
205
206 #[deprecated(note = "use read_from_file")]
208 pub fn read_from_aiff_file(file: impl StorageFile) -> crate::Result<Tag> {
209 chunk::load_id3_chunk::<chunk::AiffFormat, _>(file)
210 }
211
212 #[deprecated(note = "use read_from")]
214 pub fn read_from_wav(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
215 chunk::load_id3_chunk::<chunk::WavFormat, _>(reader)
216 }
217
218 #[deprecated(note = "use read_from_path")]
220 pub fn read_from_wav_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
221 let mut file = BufReader::new(File::open(path)?);
222 chunk::load_id3_chunk::<chunk::WavFormat, _>(&mut file)
223 }
224
225 #[deprecated(note = "use read_from_file")]
227 pub fn read_from_wav_file(file: impl StorageFile) -> crate::Result<Tag> {
228 chunk::load_id3_chunk::<chunk::WavFormat, _>(file)
229 }
230
231 pub fn write_to(&self, writer: impl io::Write, version: Version) -> crate::Result<()> {
236 stream::tag::Encoder::new()
237 .version(version)
238 .encode(self, writer)
239 }
240
241 pub fn write_to_file(&self, file: impl StorageFile, version: Version) -> crate::Result<()> {
245 stream::tag::Encoder::new()
246 .version(version)
247 .write_to_file(self, file)?;
248 Ok(())
249 }
250
251 pub fn write_to_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
253 let file = fs::OpenOptions::new().read(true).write(true).open(path)?;
254 self.write_to_file(file, version)
255 }
256
257 #[deprecated(note = "use write_to_path")]
259 pub fn write_to_aiff_path(
260 &self,
261 path: impl AsRef<Path>,
262 version: Version,
263 ) -> crate::Result<()> {
264 let mut file = fs::OpenOptions::new()
265 .read(true)
266 .write(true)
267 .create(false)
268 .truncate(false)
269 .open(path)?;
270 chunk::write_id3_chunk_file::<chunk::AiffFormat>(&mut file, self, version)?;
271 file.flush()?;
272 Ok(())
273 }
274
275 #[deprecated(note = "use write_to_file")]
277 pub fn write_to_aiff_file(
278 &self,
279 file: impl StorageFile,
280 version: Version,
281 ) -> crate::Result<()> {
282 chunk::write_id3_chunk_file::<chunk::AiffFormat>(file, self, version)
283 }
284
285 #[deprecated(note = "use write_to_path")]
287 pub fn write_to_wav_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
288 let mut file = fs::OpenOptions::new()
289 .read(true)
290 .write(true)
291 .create(false)
292 .truncate(false)
293 .open(path)?;
294 chunk::write_id3_chunk_file::<chunk::WavFormat>(&mut file, self, version)?;
295 file.flush()?;
296 Ok(())
297 }
298
299 #[deprecated(note = "use write_to_file")]
301 pub fn write_to_wav_file(&self, file: impl StorageFile, version: Version) -> crate::Result<()> {
302 chunk::write_id3_chunk_file::<chunk::WavFormat>(file, self, version)
303 }
304
305 pub fn version(&self) -> Version {
307 self.version
308 }
309
310 pub fn frames(&'a self) -> impl Iterator<Item = &'a Frame> + 'a {
324 self.frames.iter()
325 }
326
327 pub fn extended_texts(&'a self) -> impl Iterator<Item = &'a ExtendedText> + 'a {
329 self.frames()
330 .filter_map(|frame| frame.content().extended_text())
331 }
332
333 pub fn extended_links(&'a self) -> impl Iterator<Item = &'a ExtendedLink> + 'a {
335 self.frames()
336 .filter_map(|frame| frame.content().extended_link())
337 }
338
339 pub fn encapsulated_objects(&'a self) -> impl Iterator<Item = &'a EncapsulatedObject> + 'a {
341 self.frames()
342 .filter_map(|frame| frame.content().encapsulated_object())
343 }
344 pub fn comments(&'a self) -> impl Iterator<Item = &'a Comment> + 'a {
370 self.frames().filter_map(|frame| frame.content().comment())
371 }
372
373 pub fn lyrics(&'a self) -> impl Iterator<Item = &'a Lyrics> + 'a {
375 self.frames().filter_map(|frame| frame.content().lyrics())
376 }
377
378 pub fn synchronised_lyrics(&'a self) -> impl Iterator<Item = &'a SynchronisedLyrics> + 'a {
380 self.frames()
381 .filter_map(|frame| frame.content().synchronised_lyrics())
382 }
383
384 pub fn pictures(&'a self) -> impl Iterator<Item = &'a Picture> + 'a {
405 self.frames().filter_map(|frame| frame.content().picture())
406 }
407
408 pub fn unique_file_identifiers(
427 &'a self,
428 ) -> impl Iterator<Item = &'a UniqueFileIdentifier> + 'a {
429 self.frames()
430 .filter_map(|frame| frame.content().unique_file_identifier())
431 }
432
433 pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
460 self.frames().filter_map(|frame| frame.content().chapter())
461 }
462
463 pub fn tables_of_contents(&self) -> impl Iterator<Item = &TableOfContents> {
496 self.frames()
497 .filter_map(|frame| frame.content().table_of_contents())
498 }
499
500 pub fn involved_people_lists(&self) -> impl Iterator<Item = &InvolvedPeopleList> {
585 self.frames()
586 .filter_map(|frame| frame.content().involved_people_list())
587 }
588}
589
590impl PartialEq for Tag {
591 fn eq(&self, other: &Tag) -> bool {
592 self.frames.len() == other.frames.len()
593 && self.frames().all(|frame| other.frames.contains(frame))
594 }
595}
596
597impl FromIterator<Frame> for Tag {
598 fn from_iter<I: IntoIterator<Item = Frame>>(iter: I) -> Self {
599 Self {
600 frames: Vec::from_iter(iter),
601 ..Self::default()
602 }
603 }
604}
605
606impl Extend<Frame> for Tag {
607 fn extend<I: IntoIterator<Item = Frame>>(&mut self, iter: I) {
608 self.frames.extend(iter)
609 }
610}
611
612impl TagLike for Tag {
613 fn frames_vec(&self) -> &Vec<Frame> {
614 &self.frames
615 }
616
617 fn frames_vec_mut(&mut self) -> &mut Vec<Frame> {
618 &mut self.frames
619 }
620}
621
622impl From<v1::Tag> for Tag {
623 fn from(tag_v1: v1::Tag) -> Tag {
624 let mut tag = Tag::new();
625 if let Some(genre) = tag_v1.genre() {
626 tag.set_genre(genre.to_string());
627 }
628 if !tag_v1.title.is_empty() {
629 tag.set_title(tag_v1.title);
630 }
631 if !tag_v1.artist.is_empty() {
632 tag.set_artist(tag_v1.artist);
633 }
634 if !tag_v1.album.is_empty() {
635 tag.set_album(tag_v1.album);
636 }
637 if !tag_v1.year.is_empty() {
638 tag.set_text("TYER", tag_v1.year);
639 }
640 if !tag_v1.comment.is_empty() {
641 tag.add_frame(Comment {
642 lang: "eng".to_string(),
643 description: "".to_string(),
644 text: tag_v1.comment,
645 });
646 }
647 if let Some(track) = tag_v1.track {
648 tag.set_track(u32::from(track));
649 }
650 tag
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657 use crate::taglike::TagLike;
658 use std::error::Error;
659 use std::fs;
660 use std::process::Command;
661 use std::{self, io::Read, io::Seek};
662 use tempfile::tempdir;
663
664 fn ffprobe(file: impl AsRef<Path>) -> Result<String, Box<dyn Error>> {
665 let output = Command::new("ffprobe")
666 .arg(file.as_ref())
667 .output()
668 .map_err(|err| match err.kind() {
669 io::ErrorKind::NotFound => io::Error::new(
670 io::ErrorKind::NotFound,
671 format!("{}. Is ffprobe present in $PATH?", err),
672 ),
673 _ => err,
674 })?;
675 if !output.status.success() {
676 return Err(Box::new(io::Error::new(
677 io::ErrorKind::Other,
678 "ffprobe output status failure",
679 )));
680 }
681 Ok(String::from_utf8(output.stderr)?)
682 }
683
684 #[test]
685 fn remove_id3v2() {
686 let tmp = tempdir().unwrap();
687 let tmp_name = tmp.path().join("remove_id3v2_tag");
688 {
689 let mut tag_file = fs::File::create(&tmp_name).unwrap();
690 let mut original = fs::File::open("testdata/id3v24.id3").unwrap();
691 io::copy(&mut original, &mut tag_file).unwrap();
692 }
693 let mut tag_file = fs::OpenOptions::new()
694 .read(true)
695 .write(true)
696 .open(&tmp_name)
697 .unwrap();
698 tag_file.seek(io::SeekFrom::Start(0)).unwrap();
699 assert!(Tag::remove_from_file(&mut tag_file).unwrap());
700 tag_file.seek(io::SeekFrom::Start(0)).unwrap();
701 assert!(!Tag::remove_from_file(&mut tag_file).unwrap());
702 }
703
704 #[test]
706 fn test_issue_39() {
707 let tmp = tempfile::NamedTempFile::new().unwrap();
709 fs::copy("testdata/quiet.mp3", &tmp).unwrap();
710 let mut tag = Tag::new();
712 tag.set_title("Title");
713 tag.set_artist("Artist");
714 tag.write_to_path(&tmp, Version::Id3v24).unwrap();
715 let output = ffprobe(&tmp).unwrap();
717 assert!(!output.contains("Estimating duration from bitrate, this may be inaccurate"));
719 assert!(!output.contains("bytes of junk at"));
720 println!("{}", output);
722 }
723
724 #[test]
725 fn github_issue_82() {
726 let mut tag = Tag::new();
727 tag.set_artist("artist 1\0artist 2\0artist 3");
728 assert_eq!(tag.artist(), Some("artist 1\0artist 2\0artist 3"));
729 let mut buf = Vec::new();
730 tag.write_to(&mut buf, Version::Id3v22).unwrap();
731 let tag = Tag::read_from2(io::Cursor::new(buf)).unwrap();
732 assert_eq!(tag.artist(), Some("artist 1\0artist 2\0artist 3"));
733 }
734
735 #[test]
736 fn github_issue_86a() {
737 let _tag = Tag::read_from_path("testdata/github-issue-86a.id3").unwrap();
739 }
740
741 #[test]
742 fn github_issue_86c() {
743 let _tag = Tag::read_from_path("testdata/github-issue-86b.id3").unwrap();
746 }
747
748 #[test]
749 fn github_issue_91() {
750 let _tag = Tag::read_from_path("testdata/github-issue-91.id3").unwrap();
752 }
753
754 #[test]
755 fn github_issue_147() {
756 let _tag = Tag::read_from_path("testdata/github-issue-147.id3").unwrap();
760 }
761
762 #[test]
763 fn github_issue_156a() {
764 let tmp = tempfile::NamedTempFile::new().unwrap();
765 std::fs::copy("testdata/github-issue-156a.id3", &tmp).unwrap();
766
767 let tag = crate::partial_tag_ok(Tag::read_from_path(&tmp)).unwrap();
768 tag.write_to_path(&tmp, Version::Id3v23).unwrap();
769 }
770
771 #[test]
772 fn github_issue_156b() {
773 let tmp = tempfile::NamedTempFile::new().unwrap();
774 std::fs::copy("testdata/github-issue-156b.id3", &tmp).unwrap();
775
776 let tag = Tag::read_from_path(&tmp).unwrap();
777 tag.write_to_path(&tmp, Version::Id3v23).unwrap();
778 }
779
780 #[test]
781 fn aiff_read_and_write() {
782 let tmp = tempfile::NamedTempFile::new().unwrap();
784 std::fs::copy("testdata/aiff/quiet.aiff", &tmp).unwrap();
785
786 let mut tag = Tag::read_from2(&tmp).unwrap();
788 assert_eq!(tag.title(), Some("Title"));
789 assert_eq!(tag.album(), Some("Album"));
790
791 tag.set_title("NewTitle");
793 tag.set_album("NewAlbum");
794
795 tag.write_to_path(&tmp, Version::Id3v24).unwrap();
797
798 let output = ffprobe(&tmp).unwrap();
800 assert!(!output.contains("Input/output error"));
801 println!("{}", output);
803
804 tag = Tag::read_from_path(&tmp).unwrap();
806 assert_eq!(tag.title(), Some("NewTitle"));
807 assert_eq!(tag.album(), Some("NewAlbum"));
808 }
809
810 #[test]
811 fn aiff_read_padding() {
812 let tag = Tag::read_from_path("testdata/aiff/padding.aiff").unwrap();
813
814 assert_eq!(tag.title(), Some("TEST TITLE"));
815 assert_eq!(tag.artist(), Some("TEST ARTIST"));
816 }
817
818 #[test]
819 fn wav_read_tagless() {
820 use crate::ErrorKind;
821
822 let error = Tag::read_from_path("testdata/wav/tagless.wav").unwrap_err();
823
824 assert!(
825 matches!(error.kind, ErrorKind::NoTag),
826 "unexpected error kind: {:?}",
827 error.kind
828 );
829 }
830
831 #[test]
832 fn wav_read_tag_mid() {
833 let tag = Tag::read_from_path("testdata/wav/tagged-mid.wav").unwrap();
834
835 assert_eq!(tag.title(), Some("Some Great Song"));
836 assert_eq!(tag.artist(), Some("Some Great Band"));
837 if cfg!(feature = "decode_picture") {
838 assert!(tag.pictures().next().is_some())
839 }
840 }
841
842 #[test]
843 fn wav_read_tag_end() {
844 let tag = Tag::read_from_path("testdata/wav/tagged-end.wav").unwrap();
845
846 assert_eq!(tag.title(), Some("Some Great Song"));
847 assert_eq!(tag.artist(), Some("Some Great Band"));
848 if cfg!(feature = "decode_picture") {
849 assert!(tag.pictures().next().is_some())
850 }
851 }
852
853 #[test]
854 fn wav_read_tagless_corrupted() {
855 use crate::ErrorKind;
856
857 let error = Tag::read_from_path("testdata/wav/tagless-corrupted.wav").unwrap_err();
858
859 assert!(
861 matches!(error.kind, ErrorKind::Io(ref error) if error.kind() == io::ErrorKind::UnexpectedEof),
862 "unexpected error kind: {:?}",
863 error.kind
864 );
865
866 let error = Tag::read_from_path("testdata/wav/tagless-corrupted-2.wav").unwrap_err();
867
868 assert!(
870 matches!(error.kind, ErrorKind::InvalidInput),
871 "unexpected error kind: {:?}",
872 error.kind
873 );
874 }
875
876 #[test]
877 fn wav_read_tag_corrupted() {
878 use crate::ErrorKind;
879
880 let error = Tag::read_from_path("testdata/wav/tagged-mid-corrupted.wav").unwrap_err();
881
882 assert!(
883 matches!(error.kind, ErrorKind::NoTag),
884 "unexpected error kind: {:?}",
885 error.kind
886 );
887 }
888
889 #[test]
890 fn wav_read_trailing_data() {
891 use crate::ErrorKind;
892
893 let error = Tag::read_from_path("testdata/wav/tagless-trailing-data.wav").unwrap_err();
894
895 assert!(
896 matches!(error.kind, ErrorKind::NoTag),
897 "unexpected error kind: {:?}",
898 error.kind
899 );
900 }
901
902 #[test]
903 fn wav_write_tagged_end() {
904 let tmp = tempfile::NamedTempFile::new().unwrap();
905 std::fs::copy("testdata/wav/tagged-end.wav", &tmp).unwrap();
906
907 edit_and_check_wav_tag(&tmp, &tmp).unwrap();
908 }
909
910 #[test]
911 fn wav_write_tagged_mid() {
912 let tmp = tempfile::NamedTempFile::new().unwrap();
913 std::fs::copy("testdata/wav/tagged-mid.wav", &tmp).unwrap();
914
915 edit_and_check_wav_tag(&tmp, &tmp).unwrap();
916
917 let mut file = File::open(&tmp).unwrap();
918
919 check_trailing_data(&mut file, b"data\x12\0\0\0here is some music");
920 }
921
922 #[test]
923 fn wav_write_tagless() {
924 let tmp = tempfile::NamedTempFile::new().unwrap();
925 std::fs::copy("testdata/wav/tagless.wav", &tmp).unwrap();
926
927 edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap();
928 }
929
930 #[test]
931 fn wav_write_trailing_data() {
932 let tmp = tempfile::NamedTempFile::new().unwrap();
933 std::fs::copy("testdata/wav/tagless-trailing-data.wav", &tmp).unwrap();
934
935 edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap();
936
937 let mut file = File::open(&tmp).unwrap();
938
939 check_trailing_data(
940 &mut file,
941 b", and here is some trailing data that should be preserved.",
942 );
943 }
944
945 #[test]
946 fn wav_write_corrupted() {
947 use crate::ErrorKind;
948
949 let tmp = tempfile::NamedTempFile::new().unwrap();
950 std::fs::copy("testdata/wav/tagless-corrupted.wav", &tmp).unwrap();
951
952 let error = edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap_err();
953
954 assert!(
956 matches!(error.kind, ErrorKind::Io(ref error) if error.kind() == io::ErrorKind::UnexpectedEof),
957 "unexpected error kind: {:?}",
958 error.kind
959 );
960
961 let tmp = tempfile::NamedTempFile::new().unwrap();
962 std::fs::copy("testdata/wav/tagless-corrupted-2.wav", &tmp).unwrap();
963
964 let error = edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap_err();
965
966 assert!(
968 matches!(error.kind, ErrorKind::InvalidInput),
969 "unexpected error kind: {:?}",
970 error.kind
971 );
972 }
973
974 fn edit_and_check_wav_tag(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
975 let from = from.as_ref();
976 let to = to.as_ref();
977
978 let mut tag = Tag::read_from_path(from)?;
980
981 tag.set_title("NewTitle");
983 tag.set_album("NewAlbum");
984 tag.set_genre("New Wave");
985 tag.set_disc(20);
986 tag.set_duration(500);
987 tag.set_year(2020);
988
989 tag.write_to_path(to, Version::Id3v24)?;
991
992 tag = Tag::read_from_path(to)?;
994 assert_eq!(tag.title(), Some("NewTitle"));
995 assert_eq!(tag.album(), Some("NewAlbum"));
996 assert_eq!(tag.genre(), Some("New Wave"));
997 assert_eq!(tag.disc(), Some(20));
998 assert_eq!(tag.duration(), Some(500));
999 assert_eq!(tag.year(), Some(2020));
1000
1001 Ok(())
1002 }
1003
1004 fn check_trailing_data<const N: usize>(file: &mut File, data: &[u8; N]) {
1005 let mut trailing_data = [0; N];
1006 file.seek(io::SeekFrom::End(-(N as i64))).unwrap();
1007
1008 file.read_exact(&mut trailing_data).unwrap();
1009
1010 assert_eq!(&trailing_data, data)
1011 }
1012
1013 #[test]
1014 fn check_read_version() {
1015 assert_eq!(
1016 Tag::read_from_path("testdata/id3v22.id3")
1017 .unwrap()
1018 .version(),
1019 Version::Id3v22
1020 );
1021 assert_eq!(
1022 Tag::read_from_path("testdata/id3v23.id3")
1023 .unwrap()
1024 .version(),
1025 Version::Id3v23
1026 );
1027 assert_eq!(
1028 Tag::read_from_path("testdata/id3v24.id3")
1029 .unwrap()
1030 .version(),
1031 Version::Id3v24
1032 );
1033 }
1034
1035 #[test]
1036 fn test_sylt() {
1037 let tag = Tag::read_from_path("testdata/SYLT.mp3").unwrap();
1038 let lyrics = tag.synchronised_lyrics().next().unwrap();
1039 assert_eq!(lyrics.description, "Description");
1040 }
1041
1042 #[test]
1043 fn test_issue_84() {
1044 let tag = Tag::read_from_path("testdata/multi-tags.mp3").unwrap();
1046 let genres = tag.genres();
1047 let artists = tag.artists();
1048
1049 assert_eq!(genres, Some(vec!["Pop/Trip-Hop"]));
1050 assert_eq!(artists, Some(vec!["First", "Secondary"]));
1051 }
1052
1053 #[test]
1055 fn test_serato_geob() {
1056 let tag = Tag::read_from_path("testdata/geob_serato.id3").unwrap();
1057 let count = tag.encapsulated_objects().count();
1058 assert_eq!(count, 14);
1059 tag.write_to_path("testdata/geob_serato.id3", Version::Id3v24)
1060 .unwrap();
1061 let tag = Tag::read_from_path("testdata/geob_serato.id3").unwrap();
1062 assert_eq!(count, tag.encapsulated_objects().count());
1063 }
1064
1065 #[test]
1068 fn test_ipls_id3v23_utf16() {
1069 let tag = Tag::read_from_path("testdata/picard-2.12.3-id3v23-utf16.id3").unwrap();
1070 assert_eq!(tag.version(), Version::Id3v23);
1071 let count = tag.involved_people_lists().count();
1072 assert_eq!(count, 1);
1073 let ipls = tag.get("IPLS").unwrap();
1074 let involved_people = ipls
1075 .content()
1076 .involved_people_list()
1077 .unwrap()
1078 .items
1079 .iter()
1080 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1081 .collect::<Vec<_>>();
1082 assert_eq!(
1083 &involved_people,
1084 &[
1085 ("double bass", "Israel Crosby"),
1086 ("drums (drum set)", "Vernell Fournier"),
1087 ("piano", "Ahmad Jamal"),
1088 ("producer", "Dave Usher")
1089 ]
1090 );
1091
1092 let mut buffer = Vec::new();
1094 tag.write_to(&mut buffer, Version::Id3v23).unwrap();
1095 let new_tag = Tag::read_from2(io::Cursor::new(&buffer)).unwrap();
1096
1097 let new_involved_people = new_tag
1098 .get("IPLS")
1099 .unwrap()
1100 .content()
1101 .involved_people_list()
1102 .unwrap()
1103 .items
1104 .iter()
1105 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1106 .collect::<Vec<_>>();
1107 assert_eq!(&involved_people, &new_involved_people,);
1108 }
1109
1110 #[test]
1113 fn test_ipls_id3v24_utf8() {
1114 let tag = Tag::read_from_path("testdata/picard-2.12.3-id3v24-utf8.id3").unwrap();
1115 assert_eq!(tag.version(), Version::Id3v24);
1116 let count = tag.involved_people_lists().count();
1117 assert_eq!(count, 2);
1118
1119 let tipl = tag.get("TIPL").unwrap();
1120 let involved_people = tipl
1121 .content()
1122 .involved_people_list()
1123 .unwrap()
1124 .items
1125 .iter()
1126 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1127 .collect::<Vec<_>>();
1128 assert_eq!(&involved_people, &[("producer", "Dave Usher")]);
1129
1130 let tmcl = tag.get("TMCL").unwrap();
1131 let musician_credits = tmcl
1132 .content()
1133 .involved_people_list()
1134 .unwrap()
1135 .items
1136 .iter()
1137 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1138 .collect::<Vec<_>>();
1139 assert_eq!(
1140 &musician_credits,
1141 &[
1142 ("double bass", "Israel Crosby"),
1143 ("drums (drum set)", "Vernell Fournier"),
1144 ("piano", "Ahmad Jamal")
1145 ]
1146 );
1147
1148 let mut buffer = Vec::new();
1150 tag.write_to(&mut buffer, Version::Id3v24).unwrap();
1151 let new_tag = Tag::read_from2(io::Cursor::new(&buffer)).unwrap();
1152
1153 let new_involved_people = new_tag
1154 .get("TIPL")
1155 .unwrap()
1156 .content()
1157 .involved_people_list()
1158 .unwrap()
1159 .items
1160 .iter()
1161 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1162 .collect::<Vec<_>>();
1163 assert_eq!(&involved_people, &new_involved_people,);
1164
1165 let new_musician_credits = new_tag
1166 .get("TMCL")
1167 .unwrap()
1168 .content()
1169 .involved_people_list()
1170 .unwrap()
1171 .items
1172 .iter()
1173 .map(|item| (item.involvement.as_str(), item.involvee.as_str()))
1174 .collect::<Vec<_>>();
1175 assert_eq!(&musician_credits, &new_musician_credits,);
1176 }
1177}