Skip to content

Commit 87b604e

Browse files
committed
ItemKey: Add ItemKey::UnsyncLyrics
closes #561
1 parent 3d179f8 commit 87b604e

File tree

2 files changed

+84
-10
lines changed

2 files changed

+84
-10
lines changed

CHANGELOG.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77
## [Unreleased]
88

99
### Added
10-
- **ItemKey**: `ItemKey::AlbumArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/pull/523))
11-
- This is a multi-value item that stores each artist for a track. It should be retrieved with `Tag::get_strings` or `Tag::take_strings`.
12-
- For example, a track has `ItemKey::TrackArtist` = "Foo & Bar", then `ItemKey::AlbumArtists` = ["Foo", "Bar"].
10+
- **ItemKey**:
11+
- `ItemKey::AlbumArtists`, available for ID3v2, Vorbis Comments, APE, and MP4 Ilst ([PR](https://github.com/Serial-ATA/lofty-rs/pull/523))
12+
- This is a multi-value item that stores each artist for a track. It should be retrieved with `Tag::get_strings` or `Tag::take_strings`.
13+
- For example, a track has `ItemKey::TrackArtist` = "Foo & Bar", then `ItemKey::AlbumArtists` = ["Foo", "Bar"].
14+
- `ItemKey::UnsyncLyrics` ([issue](https://github.com/Serial-ATA/lofty-rs/issues/561)) ([PR](https://github.com/Serial-ATA/lofty-rs/pull/568))
15+
- In formats like Vorbis Comments, `ItemKey::Lyrics` may actually contain synchronized lyrics in LRC format. To help with the ambiguity, some
16+
apps may write a separate field containing normal, unsynchronized lyrics.
17+
- In other formats where the difference doesn't matter (like ID3v2), this will act exactly the same as `ItemKey::Lyrics`.
1318
- **Serde**: [Serde] support for `*Type` enums (`FileType`, `TagType`, `PictureType`)
1419
- Support can be enabled with the new `serde` feature (not enabled by default)
1520
- **Probe**: `Probe::read_bound()` ([PR](https://github.com/Serial-ATA/lofty-rs/pull/557))

lofty/src/tag/item.rs

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,18 @@ pub(crate) use first_key;
2626
// Some formats have multiple keys that map to the same ItemKey variant, which can be added with '|'.
2727
// The standard key(s) **must** come before any popular non-standard keys.
2828
// Keys should appear in order of popularity.
29+
//
30+
// The inverse is also true, where a single key may apply to multiple ItemKey variants. The most applicable
31+
// variant must appear first.
2932
macro_rules! gen_map {
30-
($(#[$meta:meta])? $NAME:ident; $($($key:literal)|+ => $variant:ident),+) => {
33+
(
34+
$(#[$meta:meta])?
35+
$NAME:ident;
36+
37+
$(
38+
$($key:literal)|+ => $($variant:ident)|+
39+
),+ $(,)?
40+
) => {
3141
paste::paste! {
3242
$(#[$meta])?
3343
#[allow(non_camel_case_types)]
@@ -36,22 +46,23 @@ macro_rules! gen_map {
3646
$(#[$meta])?
3747
impl $NAME {
3848
pub(crate) fn get_item_key(&self, key: &str) -> Option<ItemKey> {
39-
static INSTANCE: std::sync::OnceLock<HashMap<&'static str, ItemKey>> = std::sync::OnceLock::new();
49+
static INSTANCE: std::sync::OnceLock<HashMap<&'static str, &'static [ItemKey]>> = std::sync::OnceLock::new();
4050
INSTANCE.get_or_init(|| {
4151
let mut map = HashMap::new();
4252
$(
53+
let values: &'static [ItemKey] = &[$(ItemKey::$variant,)+];
4354
$(
44-
map.insert($key, ItemKey::$variant);
55+
map.insert($key, values);
4556
)+
4657
)+
4758
map
48-
}).iter().find(|(k, _)| k.eq_ignore_ascii_case(key)).map(|(_, v)| v.clone())
59+
}).iter().find(|(k, _)| k.eq_ignore_ascii_case(key)).map(|(_, v)| v[0])
4960
}
5061

5162
pub(crate) fn get_key(&self, item_key: ItemKey) -> Option<&'static str> {
5263
match item_key {
5364
$(
54-
ItemKey::$variant => Some(first_key!($($key)|*)),
65+
$(ItemKey::$variant)|+ => Some(first_key!($($key)|*)),
5566
)+
5667
_ => None
5768
}
@@ -128,6 +139,7 @@ gen_map!(
128139
"language" => Language,
129140
"Script" => Script,
130141
"Lyrics" => Lyrics,
142+
"UnsynchedLyrics" => UnsyncLyrics,
131143
"MUSICBRAINZ_TRACKID" => MusicBrainzRecordingId,
132144
"MUSICBRAINZ_RELEASETRACKID" => MusicBrainzTrackId,
133145
"MUSICBRAINZ_ALBUMID" => MusicBrainzReleaseId,
@@ -222,7 +234,10 @@ gen_map!(
222234
"TKWD" => PodcastKeywords,
223235
"COMM" => Comment,
224236
"TLAN" => Language,
225-
"USLT" => Lyrics,
237+
// Since ID3v2 has its own standard for synchronized lyrics (SYLT frame), and it'd be out of scope
238+
// to attempt to parse and convert LRC text into one, we can just treat both `Lyrics` and `UnsyncLyrics`
239+
// the same and map them to USLT.
240+
"USLT" => Lyrics | UnsyncLyrics,
226241
// Mapping of MusicBrainzRecordingId is implemented as a special case
227242
"MusicBrainz Release Track Id" => MusicBrainzTrackId,
228243
"MusicBrainz Album Id" => MusicBrainzReleaseId,
@@ -303,7 +318,9 @@ gen_map!(
303318
"desc" => Description,
304319
"----:com.apple.iTunes:LANGUAGE" => Language,
305320
"----:com.apple.iTunes:SCRIPT" => Script,
306-
"\u{a9}lyr" => Lyrics,
321+
// Don't know of any key for synchronized lyrics, nor if any apps actually support them, so
322+
// just treat both keys the same like ID3v2.
323+
"\u{a9}lyr" => Lyrics | UnsyncLyrics,
307324
"xid " => AppleXid,
308325
"----:com.apple.iTunes:MusicBrainz Track Id" => MusicBrainzRecordingId,
309326
"----:com.apple.iTunes:MusicBrainz Release Track Id" => MusicBrainzTrackId,
@@ -407,6 +424,7 @@ gen_map!(
407424
"LANGUAGE" => Language,
408425
"SCRIPT" => Script,
409426
"LYRICS" => Lyrics,
427+
"UNSYNCEDLYRICS" => UnsyncLyrics,
410428
"MUSICBRAINZ_TRACKID" => MusicBrainzRecordingId,
411429
"MUSICBRAINZ_RELEASETRACKID" => MusicBrainzTrackId,
412430
"MUSICBRAINZ_ALBUMID" => MusicBrainzReleaseId,
@@ -709,7 +727,35 @@ gen_item_keys!(
709727
Description,
710728
Language,
711729
Script,
730+
/// (Possibly synchronized) lyrics text
731+
///
732+
/// Despite not being specified, this field has been overloaded in some formats (such as Vorbis Comments)
733+
/// to store both synchronized lyrics (in [LRC format]) and unsynchronized lyrics.
734+
///
735+
/// Unfortunately, the best way to handle this field is to just attempt to parse it as LRC.
736+
///
737+
/// See [`ItemKey::UnsyncLyrics`] for lyrics that are *guaranteed* to be unsynchronized.
738+
///
739+
/// ## Note for ID3v2
740+
///
741+
/// ID3v2 is the only format that has a *specified* way of storing synchronized lyrics, and
742+
/// with it being a binary frame, it's only supported through [`Id3v2Tag`] via [`SynchronizedTextFrame`].
743+
/// Both [`ItemKey::Lyrics`] and [`ItemKey::UnsyncLyrics`] will be stored in a `USLT` frame.
744+
///
745+
/// [LRC format]: https://en.wikipedia.org/wiki/LRC_(file_format)
746+
/// [`Id3v2Tag`]: crate::id3::v2::Id3v2Tag
747+
/// [`SynchronizedTextFrame`]: crate::id3::v2::SynchronizedTextFrame
712748
Lyrics,
749+
/// Unsynchronized lyrics text
750+
///
751+
/// Unlike [`ItemKey::Lyrics`], this is *guaranteed* to be unsynchronized lyrics (no timecodes).
752+
///
753+
/// This only has special meaning in some formats, mapping to a separate key. In others, it's
754+
/// identical to [`ItemKey::Lyrics`].
755+
///
756+
/// You should only use this key if you're absolutely sure you need it, otherwise [`ItemKey::Lyrics`]
757+
/// is the safer default.
758+
UnsyncLyrics,
713759

714760
// Vendor-specific
715761
AppleXid,
@@ -899,3 +945,26 @@ impl TagItem {
899945
self.item_key.map_key(tag_type).is_some()
900946
}
901947
}
948+
949+
#[cfg(test)]
950+
mod tests {
951+
use super::*;
952+
953+
#[test]
954+
fn one_to_many() {
955+
assert_eq!(ItemKey::Lyrics.map_key(TagType::Id3v2), Some("USLT"));
956+
assert_eq!(ItemKey::UnsyncLyrics.map_key(TagType::Id3v2), Some("USLT"));
957+
}
958+
959+
#[test]
960+
fn many_to_one() {
961+
assert_eq!(
962+
ItemKey::from_key(TagType::VorbisComments, "ALBUMARTIST"),
963+
Some(ItemKey::AlbumArtist)
964+
);
965+
assert_eq!(
966+
ItemKey::from_key(TagType::VorbisComments, "ALBUM ARTIST"),
967+
Some(ItemKey::AlbumArtist)
968+
);
969+
}
970+
}

0 commit comments

Comments
 (0)