@@ -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.
2932macro_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