Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ID3 conversion #9

Merged
merged 2 commits into from
Nov 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -631,7 +701,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 @@ -652,7 +722,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
Loading