mirror of
https://github.com/Serial-ATA/lofty-rs
synced 2024-11-10 06:34:18 +00:00
Tags: Support ReplayGain keys
ID3v2: Properly store user defined frames after `Tag` conversion
This commit is contained in:
parent
69436e5c0a
commit
97f081fb48
3 changed files with 186 additions and 100 deletions
|
@ -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
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
158
src/tag/item.rs
158
src/tag/item.rs
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue