Skip to main content

id3/
tag.rs

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/// Denotes the version of a tag.
19#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
20pub enum Version {
21    /// ID3v2.2
22    Id3v22,
23    /// ID3v2.3
24    Id3v23,
25    /// ID3v2.4
26    #[default]
27    Id3v24,
28}
29
30impl Version {
31    /// Returns the minor version.
32    ///
33    /// # Example
34    /// ```
35    /// use id3::Version;
36    ///
37    /// assert_eq!(Version::Id3v24.minor(), 4);
38    /// ```
39    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/// An ID3 tag containing zero or more [`Frame`]s.
59#[derive(Clone, Debug, Default, Eq)]
60pub struct Tag {
61    /// A vector of frames included in the tag.
62    frames: Vec<Frame>,
63    /// ID3 Tag version
64    version: Version,
65}
66
67impl<'a> Tag {
68    /// Creates a new ID3v2.4 tag with no frames.
69    pub fn new() -> Tag {
70        Tag::default()
71    }
72
73    /// Used for creating new tag with a specific version.
74    pub fn with_version(version: Version) -> Tag {
75        Tag {
76            version,
77            ..Tag::default()
78        }
79    }
80
81    // Read/write functions are declared below. We adhere to the following naming conventions:
82    // * <format> -> io::Read/io::Write (+ io::Seek?)
83    // * <format>_path -> impl AsRef<Path>
84    // * <format>_file -> &mut File
85
86    /// Will return true if the reader is a candidate for an ID3 tag. The reader position will be
87    /// reset back to the previous position before returning.
88    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    /// Detects the presence of an ID3v2 tag at the current position of the reader and skips it
103    /// if is found. Returns true if a tag was found.
104    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    /// Removes an ID3v2 tag from the file at the specified path.
119    ///
120    /// Returns true if the file initially contained a tag.
121    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    /// Removes an ID3v2 tag from the specified file.
132    ///
133    /// Returns true if the file initially contained a tag.
134    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        // Open the ID3 region for writing and write nothing. This removes the region in its
144        // entirety.
145        let mut storage = PlainStorage::new(file, location);
146        storage.writer()?.flush()?;
147        Ok(true)
148    }
149
150    /// Attempts to read an ID3 tag from the reader.
151    #[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    /// Attempts to read an ID3 tag from the reader.
157    ///
158    /// The file format is detected using header magic.
159    ///
160    /// In the case of both Aiff/Wav tags and a ID3 header being present, the header takes
161    /// precense.
162    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    /// Attempts to read an ID3 tag via Tokio from the reader.
174    #[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    /// Attempts to read an ID3 tag from the file at the indicated path.
182    pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
183        Tag::read_from2(File::open(path)?)
184    }
185
186    /// Attempts to read an ID3 tag via Tokio from the file at the indicated path.
187    #[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    /// Reads an AIFF stream and returns any present ID3 tag.
194    #[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    /// Reads an AIFF file at the specified path and returns any present ID3 tag.
200    #[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    /// Reads an AIFF file and returns any present ID3 tag.
207    #[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    /// Reads an WAV stream and returns any present ID3 tag.
213    #[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    /// Reads an WAV file at the specified path and returns any present ID3 tag.
219    #[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    /// Reads an WAV file and returns any present ID3 tag.
226    #[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    /// Attempts to write the ID3 tag to the writer using the specified version.
232    ///
233    /// Note that the plain tag is written, regardless of the original contents. To safely encode a
234    /// tag to an MP3 file, use `Tag::write_to_file`.
235    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    /// Attempts to write the ID3 tag from the file at the indicated path. If the specified path is
242    /// the same path which the tag was read from, then the tag will be written to the padding if
243    /// possible.
244    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    /// Conventience function for [`write_to_file`].
252    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    /// Overwrite WAV file ID3 chunk in a file
258    #[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    /// Overwrite AIFF file ID3 chunk in a file. The file must be opened read/write.
276    #[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    /// Overwrite WAV file ID3 chunk
286    #[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    /// Overwrite AIFF file ID3 chunk in a file. The file must be opened read/write.
300    #[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    /// Returns version of the read tag.
306    pub fn version(&self) -> Version {
307        self.version
308    }
309
310    /// Returns an iterator over the all frames in the tag.
311    ///
312    /// # Example
313    /// ```
314    /// use id3::{Content, Frame, Tag, TagLike};
315    ///
316    /// let mut tag = Tag::new();
317    ///
318    /// tag.add_frame(Frame::with_content("TPE1", Content::Text("".to_string())));
319    /// tag.add_frame(Frame::with_content("APIC", Content::Text("".to_string())));
320    ///
321    /// assert_eq!(tag.frames().count(), 2);
322    /// ```
323    pub fn frames(&'a self) -> impl Iterator<Item = &'a Frame> + 'a {
324        self.frames.iter()
325    }
326
327    /// Returns an iterator over the extended texts in the tag.
328    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    /// Returns an iterator over the extended links in the tag.
334    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    /// Returns an iterator over the [General Encapsulated Object (GEOB)](https://id3.org/id3v2.3.0#General_encapsulated_object) frames in the tag.
340    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    /// Returns an iterator over the comments in the tag.
345    ///
346    /// # Example
347    /// ```
348    /// use id3::{Frame, Tag, TagLike};
349    /// use id3::frame::{Content, Comment};
350    ///
351    /// let mut tag = Tag::new();
352    ///
353    /// let frame = Frame::with_content("COMM", Content::Comment(Comment {
354    ///     lang: "eng".to_owned(),
355    ///     description: "key1".to_owned(),
356    ///     text: "value1".to_owned()
357    /// }));
358    /// tag.add_frame(frame);
359    ///
360    /// let frame = Frame::with_content("COMM", Content::Comment(Comment {
361    ///     lang: "eng".to_owned(),
362    ///     description: "key2".to_owned(),
363    ///     text: "value2".to_owned()
364    /// }));
365    /// tag.add_frame(frame);
366    ///
367    /// assert_eq!(tag.comments().count(), 2);
368    /// ```
369    pub fn comments(&'a self) -> impl Iterator<Item = &'a Comment> + 'a {
370        self.frames().filter_map(|frame| frame.content().comment())
371    }
372
373    /// Returns an iterator over the lyrics frames in the tag.
374    pub fn lyrics(&'a self) -> impl Iterator<Item = &'a Lyrics> + 'a {
375        self.frames().filter_map(|frame| frame.content().lyrics())
376    }
377
378    /// Returns an iterator over the synchronised lyrics frames in the tag.
379    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    /// Returns an iterator over the pictures in the tag.
385    ///
386    /// # Example
387    /// ```
388    /// use id3::{Frame, Tag, TagLike};
389    /// use id3::frame::{Content, Picture, PictureType};
390    ///
391    /// let mut tag = Tag::new();
392    ///
393    /// let picture = Picture {
394    ///     mime_type: String::new(),
395    ///     picture_type: PictureType::Other,
396    ///     description: String::new(),
397    ///     data: Vec::new(),
398    /// };
399    /// tag.add_frame(Frame::with_content("APIC", Content::Picture(picture.clone())));
400    /// tag.add_frame(Frame::with_content("APIC", Content::Picture(picture.clone())));
401    ///
402    /// assert_eq!(tag.pictures().count(), 1);
403    /// ```
404    pub fn pictures(&'a self) -> impl Iterator<Item = &'a Picture> + 'a {
405        self.frames().filter_map(|frame| frame.content().picture())
406    }
407
408    /// Returns an iterator over the Unique File Identifiers (ufid) in the tag.
409    ///
410    /// # Example
411    /// ```
412    /// use id3::{Frame, Tag, TagLike};
413    /// use id3::frame::{Content, UniqueFileIdentifier};
414    ///
415    /// let mut tag = Tag::new();
416    ///
417    /// let unique_file_identifier = UniqueFileIdentifier {
418    ///     owner_identifier: String::from("http://www.id3.org/dummy/ufid.html"),
419    ///     identifier: "7FZo5fMqyG5Ys1dm8F1FHa".into(),
420    /// };
421    /// tag.add_frame(Frame::with_content("UFID", Content::UniqueFileIdentifier(unique_file_identifier.clone())));
422    /// tag.add_frame(Frame::with_content("UFID", Content::UniqueFileIdentifier(unique_file_identifier.clone())));
423    ///
424    /// assert_eq!(tag.unique_file_identifiers().count(), 1);
425    /// ```
426    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    /// Returns an iterator over all chapters (CHAP) in the tag.
434    ///
435    /// # Example
436    /// ```
437    /// use id3::{Tag, TagLike};
438    /// use id3::frame::{Chapter, Content, Frame};
439    ///
440    /// let mut tag = Tag::new();
441    /// tag.add_frame(Chapter{
442    ///     element_id: "01".to_string(),
443    ///     start_time: 1000,
444    ///     end_time: 2000,
445    ///     start_offset: 0xff,
446    ///     end_offset: 0xff,
447    ///     frames: Vec::new(),
448    /// });
449    /// tag.add_frame(Chapter{
450    ///     element_id: "02".to_string(),
451    ///     start_time: 2000,
452    ///     end_time: 3000,
453    ///     start_offset: 0xff,
454    ///     end_offset: 0xff,
455    ///     frames: Vec::new(),
456    /// });
457    /// assert_eq!(2, tag.chapters().count());
458    /// ```
459    pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
460        self.frames().filter_map(|frame| frame.content().chapter())
461    }
462
463    /// Returns an iterator over all tables of contents (CTOC) in the tag.
464    ///
465    /// # Example
466    /// ```
467    /// use id3::{Tag, TagLike};
468    /// use id3::frame::{Chapter, TableOfContents, Content, Frame};
469    ///
470    /// let mut tag = Tag::new();
471    /// tag.add_frame(Chapter{
472    ///     element_id: "chap01".to_string(),
473    ///     start_time: 1000,
474    ///     end_time: 2000,
475    ///     start_offset: 0xff,
476    ///     end_offset: 0xff,
477    ///     frames: Vec::new(),
478    /// });
479    /// tag.add_frame(TableOfContents{
480    ///     element_id: "internalTable01".to_string(),
481    ///     top_level: false,
482    ///     ordered: false,
483    ///     elements: Vec::new(),
484    ///     frames: Vec::new(),
485    /// });
486    /// tag.add_frame(TableOfContents{
487    ///     element_id: "01".to_string(),
488    ///     top_level: true,
489    ///     ordered: true,
490    ///     elements: vec!["internalTable01".to_string(),"chap01".to_string()],
491    ///     frames: Vec::new(),
492    /// });
493    /// assert_eq!(2, tag.tables_of_contents().count());
494    /// ```
495    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    /// Returns an iterator over all involved people lists (`IPLS` in ID3v2.3, `TIPL` and `TMCL` in
501    /// ID3v2.4) in the tag.
502    ///
503    /// # Examples
504    ///
505    /// ## `IPLS` frame (ID3v2.3)
506    ///
507    /// ```
508    /// use id3::{Frame, Tag, TagLike, Version};
509    /// use id3::frame::{Content, InvolvedPeopleList, InvolvedPeopleListItem};
510    ///
511    /// let mut tag = Tag::with_version(Version::Id3v23);
512    ///
513    /// let frame = Frame::with_content("IPLS", Content::InvolvedPeopleList(InvolvedPeopleList {
514    ///     items: vec![
515    ///         InvolvedPeopleListItem {
516    ///             involvement: "drums (drum set)".to_string(),
517    ///             involvee: "Gene Krupa".to_string(),
518    ///         },
519    ///         InvolvedPeopleListItem {
520    ///             involvement: "piano".to_string(),
521    ///             involvee: "Hank Jones".to_string(),
522    ///         },
523    ///         InvolvedPeopleListItem {
524    ///             involvement: "tenor saxophone".to_string(),
525    ///             involvee: "Frank Socolow".to_string(),
526    ///         },
527    ///         InvolvedPeopleListItem {
528    ///             involvement: "tenor saxophone".to_string(),
529    ///             involvee: "Eddie Wasserman".to_string(),
530    ///         },
531    ///     ],
532    /// }));
533    /// tag.add_frame(frame);
534    /// assert_eq!(1, tag.involved_people_lists().count());
535    /// assert_eq!(4, tag.involved_people_lists().flat_map(|list| list.items.iter()).count());
536    /// ```
537    ///
538    /// ## `TIPL`/`TMCL` frames (ID3v2.4)
539    /// ```
540    /// use id3::{Frame, Tag, TagLike, Version};
541    /// use id3::frame::{Content, InvolvedPeopleList, InvolvedPeopleListItem};
542    ///
543    /// let mut tag = Tag::with_version(Version::Id3v24);
544    ///
545    /// let frame = Frame::with_content("TMCL", Content::InvolvedPeopleList(InvolvedPeopleList {
546    ///     items: vec![
547    ///         InvolvedPeopleListItem {
548    ///             involvement: "drums (drum set)".to_string(),
549    ///             involvee: "Gene Krupa".to_string(),
550    ///         },
551    ///         InvolvedPeopleListItem {
552    ///             involvement: "piano".to_string(),
553    ///             involvee: "Hank Jones".to_string(),
554    ///         },
555    ///         InvolvedPeopleListItem {
556    ///             involvement: "tenor saxophone".to_string(),
557    ///             involvee: "Frank Socolow".to_string(),
558    ///         },
559    ///         InvolvedPeopleListItem {
560    ///             involvement: "tenor saxophone".to_string(),
561    ///             involvee: "Eddie Wasserman".to_string(),
562    ///         },
563    ///     ],
564    /// }));
565    /// tag.add_frame(frame);
566    ///
567    /// let frame = Frame::with_content("TIPL", Content::InvolvedPeopleList(InvolvedPeopleList {
568    ///     items: vec![
569    ///         InvolvedPeopleListItem {
570    ///             involvement: "executive producer".to_string(),
571    ///             involvee: "Ken Druker".to_string(),
572    ///         },
573    ///         InvolvedPeopleListItem {
574    ///             involvement: "arranger".to_string(),
575    ///             involvee: "Gerry Mulligan".to_string(),
576    ///         },
577    ///     ],
578    /// }));
579    /// tag.add_frame(frame);
580    /// assert_eq!(2, tag.involved_people_lists().count());
581    /// assert_eq!(6, tag.involved_people_lists().flat_map(|list| list.items.iter()).count());
582    ///
583    /// ```
584    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    // https://github.com/polyfloyd/rust-id3/issues/39
705    #[test]
706    fn test_issue_39() {
707        // Create temp file
708        let tmp = tempfile::NamedTempFile::new().unwrap();
709        fs::copy("testdata/quiet.mp3", &tmp).unwrap();
710        // Generate sample tag
711        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        // Check with ffprobe
716        let output = ffprobe(&tmp).unwrap();
717        // This bug shows as different messages in ffprobe
718        assert!(!output.contains("Estimating duration from bitrate, this may be inaccurate"));
719        assert!(!output.contains("bytes of junk at"));
720        // Also show in console too for manual double check
721        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        // File has frame header flag bits set that are not known to the standard.
738        let _tag = Tag::read_from_path("testdata/github-issue-86a.id3").unwrap();
739    }
740
741    #[test]
742    fn github_issue_86c() {
743        // Unsynchronized bytes on frame boundary exposed that the unsync scheme was applied on the
744        // wrong level.
745        let _tag = Tag::read_from_path("testdata/github-issue-86b.id3").unwrap();
746    }
747
748    #[test]
749    fn github_issue_91() {
750        // Presence of extended header revealed bad calculation of remaining tag length.
751        let _tag = Tag::read_from_path("testdata/github-issue-91.id3").unwrap();
752    }
753
754    #[test]
755    fn github_issue_147() {
756        // Tag contains a broken IPLS frame with an odd value count. We need to handle this
757        // gracefully without failing to parse the entire tag, because these issue is apparently
758        // widespread.
759        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        // Copy
783        let tmp = tempfile::NamedTempFile::new().unwrap();
784        std::fs::copy("testdata/aiff/quiet.aiff", &tmp).unwrap();
785
786        // Read
787        let mut tag = Tag::read_from2(&tmp).unwrap();
788        assert_eq!(tag.title(), Some("Title"));
789        assert_eq!(tag.album(), Some("Album"));
790
791        // Edit
792        tag.set_title("NewTitle");
793        tag.set_album("NewAlbum");
794
795        // Write
796        tag.write_to_path(&tmp, Version::Id3v24).unwrap();
797
798        // Check if not corrupted with ffprobe
799        let output = ffprobe(&tmp).unwrap();
800        assert!(!output.contains("Input/output error"));
801        // Also show in console too for manual double check
802        println!("{}", output);
803
804        // Check written data
805        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        // With this file, we reach EOF before the expected chunk end.
860        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        // With this file, the RIFF chunk size is zero.
869        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        // With this file, we reach EOF before the expected chunk end.
955        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        // With this file, the RIFF chunk size is zero.
967        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        // Read
979        let mut tag = Tag::read_from_path(from)?;
980
981        // Edit
982        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        // Write
990        tag.write_to_path(to, Version::Id3v24)?;
991
992        // Check written data
993        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        // Read multiple tags from the file
1045        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    /// Serato writes its GEOB tags twice with different encoding.
1054    #[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    /// Read an IPLS frame with UTF-16 encording in an ID3v2.3 tag written by MusicBrainz Picard
1066    /// 2.12.3.
1067    #[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        // Now write the tag. Then check if it can be parsed and results in the same input.
1093        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    /// Read `TIPL` and `TMCL` frames with UTF-8 encording in an ID3v2.4 tag written by MusicBrainz
1111    /// Picard 2.12.3.
1112    #[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        // Now write the tag. Then check if it can be parsed and results in the same input.
1149        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}