Tags: Support ReplayGain keys

ID3v2: Properly store user defined frames after `Tag` conversion
This commit is contained in:
Serial 2022-07-21 15:33:57 -04:00
parent 69436e5c0a
commit 97f081fb48
No known key found for this signature in database
GPG key ID: DA95198DC17C4568
3 changed files with 186 additions and 100 deletions

View file

@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **FileType**: `FileType::from_ext` detects MP1/MP2 as `FileType::MP3`, allowing these files to be read with
`read_from_path`/`Probe::open`.
- **ItemKey**: `ItemKey::{REPLAYGAIN_ALBUM_GAIN, REPLAYGAIN_ALBUM_PEAK, REPLAYGAIN_TRACK_GAIN, REPLAYGAIN_TRACK_PEAK}`
### Changed
- **ID3v2**: `TXXX`/`WXXX` frames will be stored by their descriptions in `ID3v2Tag` -> `Tag` conversions
### Fixed
- **Tag**: The `Accessor::set_*` methods will stop falling through, and adding empty strings

View file

@ -488,53 +488,91 @@ impl From<ID3v2Tag> for Tag {
let mut tag = Self::new(TagType::ID3v2);
'outer: for frame in input.frames {
let id = frame.id_str();
for frame in input.frames {
let id = frame.id;
// The text pairs need some special treatment
match (id, frame.content()) {
match (id.as_str(), frame.value) {
("TRCK", FrameValue::Text { value: content, .. })
if split_pair(content, &mut tag, ItemKey::TrackNumber, ItemKey::TrackTotal)
.is_some() =>
if split_pair(
&content,
&mut tag,
ItemKey::TrackNumber,
ItemKey::TrackTotal,
)
.is_some() =>
{
continue
},
("TPOS", FrameValue::Text { value: content, .. })
if split_pair(content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
if split_pair(&content, &mut tag, ItemKey::DiscNumber, ItemKey::DiscTotal)
.is_some() =>
{
continue
},
_ => {},
}
let item_key = ItemKey::from_key(TagType::ID3v2, id);
let item_value = match frame.value {
FrameValue::Comment(LanguageFrame { content, .. })
| FrameValue::UnSyncText(LanguageFrame { content, .. })
| FrameValue::Text { value: content, .. }
| FrameValue::UserText(EncodedTextFrame { content, .. }) => {
// Store TXXX/WXXX frames by their descriptions, rather than their IDs
(
"TXXX",
FrameValue::UserText(EncodedTextFrame {
ref description,
ref content,
..
}),
) => {
let item_key = ItemKey::from_key(TagType::ID3v2, description);
for c in content.split(&['\0', '/'][..]) {
tag.items.push(TagItem::new(
item_key.clone(),
ItemValue::Text(c.to_string()),
));
}
continue 'outer;
},
FrameValue::URL(content)
| FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content),
FrameValue::Picture { picture, .. } => {
tag.push_picture(picture);
continue;
(
"WXXX",
FrameValue::UserText(EncodedTextFrame {
ref description,
ref content,
..
}),
) => {
let item_key = ItemKey::from_key(TagType::ID3v2, description);
for c in content.split(&['\0', '/'][..]) {
tag.items.push(TagItem::new(
item_key.clone(),
ItemValue::Locator(c.to_string()),
));
}
},
FrameValue::Popularimeter(_) => continue,
FrameValue::Binary(binary) => ItemValue::Binary(binary),
};
(id, value) => {
let item_key = ItemKey::from_key(TagType::ID3v2, id);
tag.items.push(TagItem::new(item_key, item_value));
let item_value = match value {
FrameValue::Comment(LanguageFrame { content, .. })
| FrameValue::UnSyncText(LanguageFrame { content, .. })
| FrameValue::Text { value: content, .. }
| FrameValue::UserText(EncodedTextFrame { content, .. }) => {
for c in content.split(&['\0', '/'][..]) {
tag.items.push(TagItem::new(
item_key.clone(),
ItemValue::Text(c.to_string()),
));
}
continue;
},
FrameValue::URL(content)
| FrameValue::UserURL(EncodedTextFrame { content, .. }) => ItemValue::Locator(content),
FrameValue::Picture { picture, .. } => {
tag.push_picture(picture);
continue;
},
FrameValue::Popularimeter(_) => continue,
FrameValue::Binary(binary) => ItemValue::Binary(binary),
};
tag.items.push(TagItem::new(item_key, item_value));
},
}
}
tag
@ -644,8 +682,8 @@ impl<'a, I: Iterator<Item = FrameRef<'a>> + 'a> Id3v2TagRef<'a, I> {
mod tests {
use crate::id3::v2::items::popularimeter::Popularimeter;
use crate::id3::v2::{
read_id3v2_header, Frame, FrameFlags, FrameID, FrameValue, ID3v2Tag, ID3v2Version,
LanguageFrame, TextEncoding,
read_id3v2_header, EncodedTextFrame, Frame, FrameFlags, FrameID, FrameValue, ID3v2Tag,
ID3v2Version, LanguageFrame, TextEncoding,
};
use crate::tag::utils::test_utils::read_path;
use crate::{
@ -1064,4 +1102,32 @@ mod tests {
fn utf16_txxx_with_single_bom() {
let _ = read_tag("tests/tags/assets/id3v2/issue_53.id3v24");
}
#[test]
fn replaygain_tag_conversion() {
let mut tag = ID3v2Tag::default();
tag.insert(
Frame::new(
"TXXX",
FrameValue::UserText(EncodedTextFrame {
encoding: TextEncoding::UTF8,
description: String::from("REPLAYGAIN_ALBUM_GAIN"),
content: String::from("-10.43 dB"),
}),
FrameFlags::default(),
)
.unwrap(),
);
let tag: Tag = tag.into();
assert_eq!(tag.item_count(), 1);
assert_eq!(
tag.items[0],
TagItem {
item_key: ItemKey::ReplayGainAlbumGain,
item_value: ItemValue::Text(String::from("-10.43 dB"))
}
);
}
}

View file

@ -110,6 +110,10 @@ gen_map!(
"Compilation" => FlagCompilation,
"Media" => OriginalMediaType,
"EncodedBy" => EncodedBy,
"REPLAYGAIN_ALBUM_GAIN" => ReplayGainAlbumGain,
"REPLAYGAIN_ALBUM_PEAK" => ReplayGainAlbumPeak,
"REPLAYGAIN_TRACK_GAIN" => ReplayGainTrackGain,
"REPLAYGAIN_TRACK_PEAK" => ReplayGainTrackPeak,
"Genre" => Genre,
"Mood" => Mood,
"Copyright" => CopyrightMessage,
@ -123,77 +127,81 @@ gen_map! (
#[cfg(feature = "id3v2")]
ID3V2_MAP;
"TALB" => AlbumTitle,
"TSST" => SetSubtitle,
"TIT1" | "GRP1" => ContentGroup,
"TIT2" => TrackTitle,
"TIT3" => TrackSubtitle,
"TOAL" => OriginalAlbumTitle,
"TOPE" => OriginalArtist,
"TOLY" => OriginalLyricist,
"TSOA" => AlbumTitleSortOrder,
"TSO2" => AlbumArtistSortOrder,
"TSOT" => TrackTitleSortOrder,
"TSOP" => TrackArtistSortOrder,
"TSOC" => ComposerSortOrder,
"TPE2" => AlbumArtist,
"TPE1" => TrackArtist,
"TEXT" => Writer,
"TCOM" => Composer,
"TPE3" => Conductor,
"TIPL" => InvolvedPeople,
"TEXT" => Lyricist,
"TMCL" => MusicianCredits,
"IPRO" => Producer,
"TPUB" => Publisher,
"TPUB" => Label,
"TRSN" => InternetRadioStationName,
"TRSO" => InternetRadioStationOwner,
"TPE4" => Remixer,
"TPOS" => DiscNumber,
"TPOS" => DiscTotal,
"TRCK" => TrackNumber,
"TRCK" => TrackTotal,
"POPM" => Popularimeter,
"TDRC" => RecordingDate,
"TDOR" => OriginalReleaseDate,
"TSRC" => ISRC,
"MVNM" => Movement,
"MVIN" => MovementIndex,
"TCMP" => FlagCompilation,
"PCST" => FlagPodcast,
"TFLT" => FileType,
"TOWN" => FileOwner,
"TDTG" => TaggingTime,
"TLEN" => Length,
"TOFN" => OriginalFileName,
"TMED" => OriginalMediaType,
"TENC" => EncodedBy,
"TSSE" => EncoderSoftware,
"TSSE" => EncoderSettings,
"TDEN" => EncodingTime,
"WOAF" => AudioFileURL,
"WOAS" => AudioSourceURL,
"WCOM" => CommercialInformationURL,
"WCOP" => CopyrightURL,
"WOAR" => TrackArtistURL,
"WORS" => RadioStationURL,
"WPAY" => PaymentURL,
"WPUB" => PublisherURL,
"TCON" => Genre,
"TLEY" => InitialKey,
"TMOO" => Mood,
"TBPM" => BPM,
"TCOP" => CopyrightMessage,
"TDES" => PodcastDescription,
"TCAT" => PodcastSeriesCategory,
"WFED" => PodcastURL,
"TDRL" => PodcastReleaseDate,
"TGID" => PodcastGlobalUniqueID,
"TKWD" => PodcastKeywords,
"COMM" => Comment,
"TLAN" => Language,
"USLT" => Lyrics
"TALB" => AlbumTitle,
"TSST" => SetSubtitle,
"TIT1" | "GRP1" => ContentGroup,
"TIT2" => TrackTitle,
"TIT3" => TrackSubtitle,
"TOAL" => OriginalAlbumTitle,
"TOPE" => OriginalArtist,
"TOLY" => OriginalLyricist,
"TSOA" => AlbumTitleSortOrder,
"TSO2" => AlbumArtistSortOrder,
"TSOT" => TrackTitleSortOrder,
"TSOP" => TrackArtistSortOrder,
"TSOC" => ComposerSortOrder,
"TPE2" => AlbumArtist,
"TPE1" => TrackArtist,
"TEXT" => Writer,
"TCOM" => Composer,
"TPE3" => Conductor,
"TIPL" => InvolvedPeople,
"TEXT" => Lyricist,
"TMCL" => MusicianCredits,
"IPRO" => Producer,
"TPUB" => Publisher,
"TPUB" => Label,
"TRSN" => InternetRadioStationName,
"TRSO" => InternetRadioStationOwner,
"TPE4" => Remixer,
"TPOS" => DiscNumber,
"TPOS" => DiscTotal,
"TRCK" => TrackNumber,
"TRCK" => TrackTotal,
"POPM" => Popularimeter,
"TDRC" => RecordingDate,
"TDOR" => OriginalReleaseDate,
"TSRC" => ISRC,
"MVNM" => Movement,
"MVIN" => MovementIndex,
"TCMP" => FlagCompilation,
"PCST" => FlagPodcast,
"TFLT" => FileType,
"TOWN" => FileOwner,
"TDTG" => TaggingTime,
"TLEN" => Length,
"TOFN" => OriginalFileName,
"TMED" => OriginalMediaType,
"TENC" => EncodedBy,
"TSSE" => EncoderSoftware,
"TSSE" => EncoderSettings,
"TDEN" => EncodingTime,
"REPLAYGAIN_ALBUM_GAIN" => ReplayGainAlbumGain,
"REPLAYGAIN_ALBUM_PEAK" => ReplayGainAlbumPeak,
"REPLAYGAIN_TRACK_GAIN" => ReplayGainTrackGain,
"REPLAYGAIN_TRACK_PEAK" => ReplayGainTrackPeak,
"WOAF" => AudioFileURL,
"WOAS" => AudioSourceURL,
"WCOM" => CommercialInformationURL,
"WCOP" => CopyrightURL,
"WOAR" => TrackArtistURL,
"WORS" => RadioStationURL,
"WPAY" => PaymentURL,
"WPUB" => PublisherURL,
"TCON" => Genre,
"TLEY" => InitialKey,
"TMOO" => Mood,
"TBPM" => BPM,
"TCOP" => CopyrightMessage,
"TDES" => PodcastDescription,
"TCAT" => PodcastSeriesCategory,
"WFED" => PodcastURL,
"TDRL" => PodcastReleaseDate,
"TGID" => PodcastGlobalUniqueID,
"TKWD" => PodcastKeywords,
"COMM" => Comment,
"TLAN" => Language,
"USLT" => Lyrics
);
gen_map! (
@ -322,6 +330,10 @@ gen_map!(
"ENCODED-BY" => EncodedBy,
"ENCODER" => EncoderSoftware,
"ENCODING" | "ENCODERSETTINGS" => EncoderSettings,
"REPLAYGAIN_ALBUM_GAIN" => ReplayGainAlbumGain,
"REPLAYGAIN_ALBUM_PEAK" => ReplayGainAlbumPeak,
"REPLAYGAIN_TRACK_GAIN" => ReplayGainTrackGain,
"REPLAYGAIN_TRACK_PEAK" => ReplayGainTrackPeak,
"GENRE" => Genre,
"MOOD" => Mood,
"BPM" => BPM,
@ -501,6 +513,10 @@ gen_item_keys!(
EncoderSoftware,
EncoderSettings,
EncodingTime,
ReplayGainAlbumGain,
ReplayGainAlbumPeak,
ReplayGainTrackGain,
ReplayGainTrackPeak,
// URLs
AudioFileURL,