use crate::chunk;
use crate::frame::{
Chapter, Comment, EncapsulatedObject, ExtendedLink, ExtendedText, Frame, Lyrics, Picture,
SynchronisedLyrics, TableOfContents,
};
use crate::storage::{plain::PlainStorage, Format, Storage};
use crate::stream;
use crate::taglike::TagLike;
use crate::v1;
use crate::StorageFile;
use crate::{Error, ErrorKind};
use std::fmt;
use std::fs::{self, File};
use std::io::{self, BufRead, BufReader, Write};
use std::iter::{FromIterator, Iterator};
use std::path::Path;
#[derive(Copy, Clone, Default, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub enum Version {
Id3v22,
Id3v23,
#[default]
Id3v24,
}
impl Version {
pub fn minor(self) -> u8 {
match self {
Version::Id3v22 => 2,
Version::Id3v23 => 3,
Version::Id3v24 => 4,
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Version::Id3v22 => write!(f, "ID3v2.2"),
Version::Id3v23 => write!(f, "ID3v2.3"),
Version::Id3v24 => write!(f, "ID3v2.4"),
}
}
}
#[derive(Clone, Debug, Default, Eq)]
pub struct Tag {
frames: Vec<Frame>,
version: Version,
}
impl<'a> Tag {
pub fn new() -> Tag {
Tag::default()
}
pub fn with_version(version: Version) -> Tag {
Tag {
version,
..Tag::default()
}
}
pub fn is_candidate(mut reader: impl io::Read + io::Seek) -> crate::Result<bool> {
let initial_position = reader.stream_position()?;
let is_candidate = match stream::tag::locate_id3v2(&mut reader) {
Ok(_) => true,
Err(Error {
kind: ErrorKind::NoTag,
..
}) => false,
Err(err) => return Err(err),
};
reader.seek(io::SeekFrom::Start(initial_position))?;
Ok(is_candidate)
}
pub fn skip(mut reader: impl io::Read + io::Seek) -> crate::Result<bool> {
let initial_position = reader.stream_position()?;
let range = match stream::tag::locate_id3v2(&mut reader) {
Ok(v) => v,
Err(Error {
kind: ErrorKind::NoTag,
..
}) => return Ok(false),
Err(err) => return Err(err),
};
reader.seek(io::SeekFrom::Start(initial_position + range.end))?;
Ok(true)
}
pub fn remove_from_path(path: impl AsRef<Path>) -> crate::Result<bool> {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(false)
.truncate(false)
.open(path)?;
Self::remove_from_file(&mut file)
}
pub fn remove_from_file(mut file: impl StorageFile) -> crate::Result<bool> {
let location = match stream::tag::locate_id3v2(&mut file) {
Ok(l) => l,
Err(Error {
kind: ErrorKind::NoTag,
..
}) => return Ok(false),
Err(err) => return Err(err),
};
let mut storage = PlainStorage::new(file, location);
storage.writer()?.flush()?;
Ok(true)
}
#[deprecated(note = "use read_from2")]
pub fn read_from(reader: impl io::Read) -> crate::Result<Tag> {
stream::tag::decode(reader)
}
pub fn read_from2(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
let mut b = BufReader::new(reader);
let probe = b.fill_buf()?;
match Format::magic(probe) {
Some(Format::Header) | None => stream::tag::decode(b),
Some(Format::Aiff) => chunk::load_id3_chunk::<chunk::AiffFormat, _>(b),
Some(Format::Wav) => chunk::load_id3_chunk::<chunk::WavFormat, _>(b),
}
}
#[cfg(feature = "tokio")]
pub async fn async_read_from(
reader: impl tokio::io::AsyncRead + std::marker::Unpin,
) -> crate::Result<Tag> {
stream::tag::async_decode(reader).await
}
pub fn read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
Tag::read_from2(File::open(path)?)
}
#[cfg(feature = "tokio")]
pub async fn async_read_from_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
let file = tokio::io::BufReader::new(tokio::fs::File::open(path).await?);
stream::tag::async_decode(file).await
}
#[deprecated(note = "use read_from")]
pub fn read_from_aiff(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
chunk::load_id3_chunk::<chunk::AiffFormat, _>(reader)
}
#[deprecated(note = "use read_from_path")]
pub fn read_from_aiff_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
let mut file = BufReader::new(File::open(path)?);
chunk::load_id3_chunk::<chunk::AiffFormat, _>(&mut file)
}
#[deprecated(note = "use read_from_file")]
pub fn read_from_aiff_file(file: impl StorageFile) -> crate::Result<Tag> {
chunk::load_id3_chunk::<chunk::AiffFormat, _>(file)
}
#[deprecated(note = "use read_from")]
pub fn read_from_wav(reader: impl io::Read + io::Seek) -> crate::Result<Tag> {
chunk::load_id3_chunk::<chunk::WavFormat, _>(reader)
}
#[deprecated(note = "use read_from_path")]
pub fn read_from_wav_path(path: impl AsRef<Path>) -> crate::Result<Tag> {
let mut file = BufReader::new(File::open(path)?);
chunk::load_id3_chunk::<chunk::WavFormat, _>(&mut file)
}
#[deprecated(note = "use read_from_file")]
pub fn read_from_wav_file(file: impl StorageFile) -> crate::Result<Tag> {
chunk::load_id3_chunk::<chunk::WavFormat, _>(file)
}
pub fn write_to(&self, writer: impl io::Write, version: Version) -> crate::Result<()> {
stream::tag::Encoder::new()
.version(version)
.encode(self, writer)
}
pub fn write_to_file(&self, file: impl StorageFile, version: Version) -> crate::Result<()> {
stream::tag::Encoder::new()
.version(version)
.write_to_file(self, file)?;
Ok(())
}
pub fn write_to_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
let file = fs::OpenOptions::new().read(true).write(true).open(path)?;
self.write_to_file(file, version)
}
#[deprecated(note = "use write_to_path")]
pub fn write_to_aiff_path(
&self,
path: impl AsRef<Path>,
version: Version,
) -> crate::Result<()> {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(false)
.truncate(false)
.open(path)?;
chunk::write_id3_chunk_file::<chunk::AiffFormat>(&mut file, self, version)?;
file.flush()?;
Ok(())
}
#[deprecated(note = "use write_to_file")]
pub fn write_to_aiff_file(
&self,
file: impl StorageFile,
version: Version,
) -> crate::Result<()> {
chunk::write_id3_chunk_file::<chunk::AiffFormat>(file, self, version)
}
#[deprecated(note = "use write_to_path")]
pub fn write_to_wav_path(&self, path: impl AsRef<Path>, version: Version) -> crate::Result<()> {
let mut file = fs::OpenOptions::new()
.read(true)
.write(true)
.create(false)
.truncate(false)
.open(path)?;
chunk::write_id3_chunk_file::<chunk::WavFormat>(&mut file, self, version)?;
file.flush()?;
Ok(())
}
#[deprecated(note = "use write_to_file")]
pub fn write_to_wav_file(&self, file: impl StorageFile, version: Version) -> crate::Result<()> {
chunk::write_id3_chunk_file::<chunk::WavFormat>(file, self, version)
}
pub fn version(&self) -> Version {
self.version
}
pub fn frames(&'a self) -> impl Iterator<Item = &'a Frame> + 'a {
self.frames.iter()
}
pub fn extended_texts(&'a self) -> impl Iterator<Item = &'a ExtendedText> + 'a {
self.frames()
.filter_map(|frame| frame.content().extended_text())
}
pub fn extended_links(&'a self) -> impl Iterator<Item = &'a ExtendedLink> + 'a {
self.frames()
.filter_map(|frame| frame.content().extended_link())
}
pub fn encapsulated_objects(&'a self) -> impl Iterator<Item = &'a EncapsulatedObject> + 'a {
self.frames()
.filter_map(|frame| frame.content().encapsulated_object())
}
pub fn comments(&'a self) -> impl Iterator<Item = &'a Comment> + 'a {
self.frames().filter_map(|frame| frame.content().comment())
}
pub fn lyrics(&'a self) -> impl Iterator<Item = &'a Lyrics> + 'a {
self.frames().filter_map(|frame| frame.content().lyrics())
}
pub fn synchronised_lyrics(&'a self) -> impl Iterator<Item = &'a SynchronisedLyrics> + 'a {
self.frames()
.filter_map(|frame| frame.content().synchronised_lyrics())
}
pub fn pictures(&'a self) -> impl Iterator<Item = &'a Picture> + 'a {
self.frames().filter_map(|frame| frame.content().picture())
}
pub fn chapters(&self) -> impl Iterator<Item = &Chapter> {
self.frames().filter_map(|frame| frame.content().chapter())
}
pub fn tables_of_contents(&self) -> impl Iterator<Item = &TableOfContents> {
self.frames()
.filter_map(|frame| frame.content().table_of_contents())
}
}
impl PartialEq for Tag {
fn eq(&self, other: &Tag) -> bool {
self.frames.len() == other.frames.len()
&& self.frames().all(|frame| other.frames.contains(frame))
}
}
impl FromIterator<Frame> for Tag {
fn from_iter<I: IntoIterator<Item = Frame>>(iter: I) -> Self {
Self {
frames: Vec::from_iter(iter),
..Self::default()
}
}
}
impl Extend<Frame> for Tag {
fn extend<I: IntoIterator<Item = Frame>>(&mut self, iter: I) {
self.frames.extend(iter)
}
}
impl TagLike for Tag {
fn frames_vec(&self) -> &Vec<Frame> {
&self.frames
}
fn frames_vec_mut(&mut self) -> &mut Vec<Frame> {
&mut self.frames
}
}
impl From<v1::Tag> for Tag {
fn from(tag_v1: v1::Tag) -> Tag {
let mut tag = Tag::new();
if let Some(genre) = tag_v1.genre() {
tag.set_genre(genre.to_string());
}
if !tag_v1.title.is_empty() {
tag.set_title(tag_v1.title);
}
if !tag_v1.artist.is_empty() {
tag.set_artist(tag_v1.artist);
}
if !tag_v1.album.is_empty() {
tag.set_album(tag_v1.album);
}
if !tag_v1.year.is_empty() {
tag.set_text("TYER", tag_v1.year);
}
if !tag_v1.comment.is_empty() {
tag.add_frame(Comment {
lang: "eng".to_string(),
description: "".to_string(),
text: tag_v1.comment,
});
}
if let Some(track) = tag_v1.track {
tag.set_track(u32::from(track));
}
tag
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::taglike::TagLike;
use std::fs;
use std::{io::Read, io::Seek};
use tempfile::tempdir;
#[test]
fn remove_id3v2() {
let tmp = tempdir().unwrap();
let tmp_name = tmp.path().join("remove_id3v2_tag");
{
let mut tag_file = fs::File::create(&tmp_name).unwrap();
let mut original = fs::File::open("testdata/id3v24.id3").unwrap();
io::copy(&mut original, &mut tag_file).unwrap();
}
let mut tag_file = fs::OpenOptions::new()
.read(true)
.write(true)
.open(&tmp_name)
.unwrap();
tag_file.seek(io::SeekFrom::Start(0)).unwrap();
assert!(Tag::remove_from_file(&mut tag_file).unwrap());
tag_file.seek(io::SeekFrom::Start(0)).unwrap();
assert!(!Tag::remove_from_file(&mut tag_file).unwrap());
}
#[test]
fn test_issue_39() {
let tmp = tempfile::NamedTempFile::new().unwrap();
fs::copy("testdata/quiet.mp3", &tmp).unwrap();
let mut tag = Tag::new();
tag.set_title("Title");
tag.set_artist("Artist");
tag.write_to_path(&tmp, Version::Id3v24).unwrap();
use std::process::Command;
let command = Command::new("ffprobe")
.arg(tmp.path().to_str().unwrap())
.output()
.unwrap();
assert!(command.status.success());
let output = String::from_utf8(command.stderr).unwrap();
assert!(!output.contains("Estimating duration from bitrate, this may be inaccurate"));
assert!(!output.contains("bytes of junk at"));
println!("{}", output);
}
#[test]
fn github_issue_82() {
let mut tag = Tag::new();
tag.set_artist("artist 1\0artist 2\0artist 3");
assert_eq!(tag.artist(), Some("artist 1\0artist 2\0artist 3"));
let mut buf = Vec::new();
tag.write_to(&mut buf, Version::Id3v22).unwrap();
let tag = Tag::read_from2(io::Cursor::new(buf)).unwrap();
assert_eq!(tag.artist(), Some("artist 1\0artist 2\0artist 3"));
}
#[test]
fn github_issue_86a() {
let _tag = Tag::read_from_path("testdata/github-issue-86a.id3").unwrap();
}
#[test]
fn github_issue_86c() {
let _tag = Tag::read_from_path("testdata/github-issue-86b.id3").unwrap();
}
#[test]
fn github_issue_91() {
let _tag = Tag::read_from_path("testdata/github-issue-91.id3").unwrap();
}
#[test]
fn aiff_read_and_write() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/aiff/quiet.aiff", &tmp).unwrap();
let mut tag = Tag::read_from2(&tmp).unwrap();
assert_eq!(tag.title(), Some("Title"));
assert_eq!(tag.album(), Some("Album"));
tag.set_title("NewTitle");
tag.set_album("NewAlbum");
tag.write_to_path(&tmp, Version::Id3v24).unwrap();
use std::process::Command;
let command = Command::new("ffprobe")
.arg(tmp.path().to_str().unwrap())
.output()
.unwrap();
assert!(command.status.success());
let output = String::from_utf8(command.stderr).unwrap();
assert!(!output.contains("Input/output error"));
println!("{}", output);
tag = Tag::read_from_path(&tmp).unwrap();
assert_eq!(tag.title(), Some("NewTitle"));
assert_eq!(tag.album(), Some("NewAlbum"));
}
#[test]
fn aiff_read_padding() {
let tag = Tag::read_from_path("testdata/aiff/padding.aiff").unwrap();
assert_eq!(tag.title(), Some("TEST TITLE"));
assert_eq!(tag.artist(), Some("TEST ARTIST"));
}
#[test]
fn wav_read_tagless() {
use crate::ErrorKind;
let error = Tag::read_from_path("testdata/wav/tagless.wav").unwrap_err();
assert!(
matches!(error.kind, ErrorKind::NoTag),
"unexpected error kind: {:?}",
error.kind
);
}
#[test]
fn wav_read_tag_mid() {
let tag = Tag::read_from_path("testdata/wav/tagged-mid.wav").unwrap();
assert_eq!(tag.title(), Some("Some Great Song"));
assert_eq!(tag.artist(), Some("Some Great Band"));
if cfg!(feature = "decode_picture") {
assert!(tag.pictures().next().is_some())
}
}
#[test]
fn wav_read_tag_end() {
let tag = Tag::read_from_path("testdata/wav/tagged-end.wav").unwrap();
assert_eq!(tag.title(), Some("Some Great Song"));
assert_eq!(tag.artist(), Some("Some Great Band"));
if cfg!(feature = "decode_picture") {
assert!(tag.pictures().next().is_some())
}
}
#[test]
fn wav_read_tagless_corrupted() {
use crate::ErrorKind;
let error = Tag::read_from_path("testdata/wav/tagless-corrupted.wav").unwrap_err();
assert!(
matches!(error.kind, ErrorKind::Io(ref error) if error.kind() == io::ErrorKind::UnexpectedEof),
"unexpected error kind: {:?}",
error.kind
);
let error = Tag::read_from_path("testdata/wav/tagless-corrupted-2.wav").unwrap_err();
assert!(
matches!(error.kind, ErrorKind::InvalidInput),
"unexpected error kind: {:?}",
error.kind
);
}
#[test]
fn wav_read_tag_corrupted() {
use crate::ErrorKind;
let error = Tag::read_from_path("testdata/wav/tagged-mid-corrupted.wav").unwrap_err();
assert!(
matches!(error.kind, ErrorKind::NoTag),
"unexpected error kind: {:?}",
error.kind
);
}
#[test]
fn wav_read_trailing_data() {
use crate::ErrorKind;
let error = Tag::read_from_path("testdata/wav/tagless-trailing-data.wav").unwrap_err();
assert!(
matches!(error.kind, ErrorKind::NoTag),
"unexpected error kind: {:?}",
error.kind
);
}
#[test]
fn wav_write_tagged_end() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagged-end.wav", &tmp).unwrap();
edit_and_check_wav_tag(&tmp, &tmp).unwrap();
}
#[test]
fn wav_write_tagged_mid() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagged-mid.wav", &tmp).unwrap();
edit_and_check_wav_tag(&tmp, &tmp).unwrap();
let mut file = File::open(&tmp).unwrap();
check_trailing_data(&mut file, b"data\x12\0\0\0here is some music");
}
#[test]
fn wav_write_tagless() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagless.wav", &tmp).unwrap();
edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap();
}
#[test]
fn wav_write_trailing_data() {
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagless-trailing-data.wav", &tmp).unwrap();
edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap();
let mut file = File::open(&tmp).unwrap();
check_trailing_data(
&mut file,
b", and here is some trailing data that should be preserved.",
);
}
#[test]
fn wav_write_corrupted() {
use crate::ErrorKind;
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagless-corrupted.wav", &tmp).unwrap();
let error = edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap_err();
assert!(
matches!(error.kind, ErrorKind::Io(ref error) if error.kind() == io::ErrorKind::UnexpectedEof),
"unexpected error kind: {:?}",
error.kind
);
let tmp = tempfile::NamedTempFile::new().unwrap();
std::fs::copy("testdata/wav/tagless-corrupted-2.wav", &tmp).unwrap();
let error = edit_and_check_wav_tag("testdata/wav/tagged-mid.wav", &tmp).unwrap_err();
assert!(
matches!(error.kind, ErrorKind::InvalidInput),
"unexpected error kind: {:?}",
error.kind
);
}
fn edit_and_check_wav_tag(from: impl AsRef<Path>, to: impl AsRef<Path>) -> crate::Result<()> {
let from = from.as_ref();
let to = to.as_ref();
let mut tag = Tag::read_from_path(from)?;
tag.set_title("NewTitle");
tag.set_album("NewAlbum");
tag.set_genre("New Wave");
tag.set_disc(20);
tag.set_duration(500);
tag.set_year(2020);
tag.write_to_path(to, Version::Id3v24)?;
tag = Tag::read_from_path(to)?;
assert_eq!(tag.title(), Some("NewTitle"));
assert_eq!(tag.album(), Some("NewAlbum"));
assert_eq!(tag.genre(), Some("New Wave"));
assert_eq!(tag.disc(), Some(20));
assert_eq!(tag.duration(), Some(500));
assert_eq!(tag.year(), Some(2020));
Ok(())
}
fn check_trailing_data<const N: usize>(file: &mut File, data: &[u8; N]) {
let mut trailing_data = [0; N];
file.seek(io::SeekFrom::End(-(N as i64))).unwrap();
file.read_exact(&mut trailing_data).unwrap();
assert_eq!(&trailing_data, data)
}
#[test]
fn check_read_version() {
assert_eq!(
Tag::read_from_path("testdata/id3v22.id3")
.unwrap()
.version(),
Version::Id3v22
);
assert_eq!(
Tag::read_from_path("testdata/id3v23.id3")
.unwrap()
.version(),
Version::Id3v23
);
assert_eq!(
Tag::read_from_path("testdata/id3v24.id3")
.unwrap()
.version(),
Version::Id3v24
);
}
#[test]
fn test_sylt() {
let tag = Tag::read_from_path("testdata/SYLT.mp3").unwrap();
let lyrics = tag.synchronised_lyrics().next().unwrap();
assert_eq!(lyrics.description, "Description");
}
#[test]
fn test_issue_84() {
let tag = Tag::read_from_path("testdata/multi-tags.mp3").unwrap();
let genres = tag.genres();
let artists = tag.artists();
assert_eq!(genres, Some(vec!["Pop", "Trip-Hop"]));
assert_eq!(artists, Some(vec!["First", "Secondary"]));
}
#[test]
fn test_serato_geob() {
let tag = Tag::read_from_path("testdata/geob_serato.id3").unwrap();
let count = tag.encapsulated_objects().count();
assert_eq!(count, 14);
tag.write_to_path("testdata/geob_serato.id3", Version::Id3v24)
.unwrap();
let tag = Tag::read_from_path("testdata/geob_serato.id3").unwrap();
assert_eq!(count, tag.encapsulated_objects().count());
}
}