Skip to content

Commit

Permalink
Merge pull request #9 from Holzhaus/id3-conversion
Browse files Browse the repository at this point in the history
ID3 conversion
  • Loading branch information
Holzhaus authored Nov 3, 2024
2 parents 667a425 + 12bc9d7 commit 9fec8ba
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 11 deletions.
55 changes: 47 additions & 8 deletions src/tag/id3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use id3::{
};
use std::borrow::{Borrow, Cow};
use std::iter;
use std::mem;
use std::path::Path;

/// ID3 frame ID.
Expand All @@ -40,13 +41,6 @@ pub struct ID3v2Tag {
}

impl ID3v2Tag {
#[cfg(test)]
pub fn new() -> Self {
ID3v2Tag {
data: id3::Tag::new(),
}
}

#[cfg(test)]
pub fn with_version(version: id3::Version) -> Self {
ID3v2Tag {
Expand Down Expand Up @@ -258,6 +252,47 @@ impl ID3v2Tag {
})
.map(|unique_file_identifier| unique_file_identifier.identifier.as_slice())
}

/// Migrate this tag to the given ID3 version.
pub fn migrate_to(&mut self, new_version: id3::Version) {
let version = self.data.version();
if version == new_version {
return;
}

// FIXME: Converting to ID3v2.2 is not supported.
if new_version == id3::Version::Id3v22 {
return;
}

log::info!("Converting ID3 tag version {version} to {new_version}");

let old_data = mem::replace(&mut self.data, id3::Tag::with_version(new_version));
for frame in old_data.frames() {
let id = frame.id();
let new_frame = match frame.id_for_version(new_version) {
Some(new_id) if new_id == id => frame.clone(),
Some(new_id) => {
log::info!("Converting ID3 frame {id} to {new_id}");
let content = frame.content().to_owned();
id3::Frame::with_content(new_id, content)
}
None => {
log::info!("Removing unsupported ID3 frame {id}");
continue;
}
};
let _unused = self.data.add_frame(new_frame);
}
}
}

impl Default for ID3v2Tag {
fn default() -> Self {
ID3v2Tag {
data: id3::Tag::with_version(id3::Version::Id3v23),
}
}
}

impl Tag for ID3v2Tag {
Expand Down Expand Up @@ -401,6 +436,10 @@ impl Tag for ID3v2Tag {
self.data.write_to_path(path, self.data.version())?;
Ok(())
}

fn maybe_as_id3v2_mut(&mut self) -> Option<&mut ID3v2Tag> {
Some(self)
}
}

#[cfg(test)]
Expand Down Expand Up @@ -430,7 +469,7 @@ mod tests {

#[test]
fn test_get_set_clear_multivalued_text() {
let mut tag = ID3v2Tag::new();
let mut tag = ID3v2Tag::default();
assert!(tag.get(TagKey::Arranger).is_none());
assert!(tag.get(TagKey::Engineer).is_none());
assert!(tag.get(TagKey::DjMixer).is_none());
Expand Down
6 changes: 6 additions & 0 deletions src/tag/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,12 @@ pub trait Tag: Send + Sync {
}
/// Write the tags to the path.
fn write(&mut self, path: &Path) -> crate::Result<()>;

/// Get mutable reference to the underlying [`id3::ID3v2Tag`] (is this is an ID3v2 tag).
#[cfg(feature = "id3")]
fn maybe_as_id3v2_mut(&mut self) -> Option<&mut id3::ID3v2Tag> {
None
}
}

/// Return a vector of tags from the file at the given path.
Expand Down
76 changes: 73 additions & 3 deletions src/taggedfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
use crate::analyzer::CompoundAnalyzerResult;
use crate::release::ReleaseLike;
use crate::tag::{read_tags_from_path, Tag, TagKey};
use crate::tag::{read_tags_from_path, Tag, TagKey, TagType};
use crate::track::{AnalyzedTrackMetadata, TrackLike};
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ffi::OsStr;
use std::fmt;
use std::mem;
use std::path::{Path, PathBuf};

/// A tagged file that contains zero or more tags.
Expand Down Expand Up @@ -50,6 +51,75 @@ impl TaggedFile {
}
}

/// Convert tags in this file.
///
/// Currently, this always converts all ID3v2.x tags to ID3v2.3.
///
/// # Panics
///
/// This function may panic with the tag type indicates an ID3v2.x tag but the
/// `Tag::maybe_as_id3v2_mut()` function returns `None`, which constitutes a programming error.
pub fn convert_tags(&mut self) {
#[cfg(feature = "id3")]
{
let (has_id3v22, has_id3v23, has_id3v24) = self.content.iter().fold(
(false, false, false),
|(mut has_id3v22, mut has_id3v23, mut has_id3v24), tag| {
#[allow(clippy::match_wildcard_for_single_variants)]
match tag.tag_type() {
TagType::ID3v22 => has_id3v22 = true,
TagType::ID3v23 => has_id3v23 = true,
TagType::ID3v24 => has_id3v24 = true,
_ => (),
}
(has_id3v22, has_id3v23, has_id3v24)
},
);
let capacity = self.content.len();
let old_content = mem::replace(&mut self.content, Vec::with_capacity(capacity));
if has_id3v23 {
old_content
.into_iter()
.filter(|tag| {
tag.tag_type() != TagType::ID3v22 && tag.tag_type() != TagType::ID3v24
})
.for_each(|tag| self.content.push(tag));
} else if has_id3v24 {
old_content
.into_iter()
.filter_map(|mut tag| {
if tag.tag_type() == TagType::ID3v24 {
tag.maybe_as_id3v2_mut()
.expect(
"ID3 tags should always return `Some()` for `maybe_as_id3()`",
)
.migrate_to(id3::Version::Id3v23);
Some(tag)
} else if tag.tag_type() == TagType::ID3v22 {
None
} else {
Some(tag)
}
})
.for_each(|tag| self.content.push(tag));
} else if has_id3v22 {
old_content
.into_iter()
.map(|mut tag| {
if tag.tag_type() == TagType::ID3v22 {
tag.maybe_as_id3v2_mut()
.expect(
"ID3 tags should always return `Some()` for `maybe_as_id3()`",
)
.migrate_to(id3::Version::Id3v23);
}
tag
})
.for_each(|tag| self.content.push(tag));
}
}
}

/// Creates a [`TaggedFile`] from the path.
///
/// # Errors
Expand Down Expand Up @@ -638,7 +708,7 @@ mod tests {
let track: &MusicBrainzTrack =
&release.media.as_ref().unwrap()[0].tracks.as_ref().unwrap()[0];

let mut tagged_file = TaggedFile::new(vec![Box::new(ID3v2Tag::new())]);
let mut tagged_file = TaggedFile::new(vec![Box::new(ID3v2Tag::default())]);
assert!(tagged_file.track_title().is_none());
assert!(tagged_file.track_artist().is_none());
assert!(tagged_file.track_number().is_none());
Expand All @@ -659,7 +729,7 @@ mod tests {

let release: MusicBrainzRelease = serde_json::from_str(MUSICBRAINZ_RELEASE_JSON).unwrap();

let tagged_file = TaggedFile::new(vec![Box::new(ID3v2Tag::new())]);
let tagged_file = TaggedFile::new(vec![Box::new(ID3v2Tag::default())]);
let tagged_file_collection = TaggedFileCollection::new(vec![tagged_file]);
assert!(tagged_file_collection.release_title().is_none());
assert!(tagged_file_collection.release_artist().is_none());
Expand Down

0 comments on commit 9fec8ba

Please sign in to comment.