@@ -2,6 +2,7 @@ use super::frame::id::FrameId;
2
2
use super :: frame:: { Frame , FrameFlags , FrameValue , EMPTY_CONTENT_DESCRIPTOR , UNKNOWN_LANGUAGE } ;
3
3
use super :: header:: { Id3v2TagFlags , Id3v2Version } ;
4
4
use crate :: error:: { LoftyError , Result } ;
5
+ use crate :: id3:: v1:: GENRES ;
5
6
use crate :: id3:: v2:: frame:: { FrameRef , MUSICBRAINZ_UFID_OWNER } ;
6
7
use crate :: id3:: v2:: items:: {
7
8
AttachedPictureFrame , CommentFrame , ExtendedTextFrame , ExtendedUrlFrame , TextInformationFrame ,
@@ -530,6 +531,22 @@ impl Id3v2Tag {
530
531
} ;
531
532
}
532
533
534
+ /// Returns all genres contained in a `TCON` frame.
535
+ ///
536
+ /// This will translate any numeric genre IDs to their textual equivalent.
537
+ /// ID3v2.4-style multi-value fields will be split as normal.
538
+ pub fn genres ( & self ) -> Option < impl Iterator < Item = & str > > {
539
+ if let Some ( Frame {
540
+ value : FrameValue :: Text ( TextInformationFrame { ref value, .. } ) ,
541
+ ..
542
+ } ) = self . get ( & GENRE_ID )
543
+ {
544
+ return Some ( GenresIter :: new ( value) ) ;
545
+ }
546
+
547
+ None
548
+ }
549
+
533
550
fn insert_number_pair (
534
551
& mut self ,
535
552
id : FrameId < ' static > ,
@@ -544,6 +561,69 @@ impl Id3v2Tag {
544
561
}
545
562
}
546
563
564
+ struct GenresIter < ' a > {
565
+ value : & ' a str ,
566
+ pos : usize ,
567
+ }
568
+
569
+ impl < ' a > GenresIter < ' a > {
570
+ pub fn new ( value : & ' a str ) -> GenresIter < ' _ > {
571
+ GenresIter { value, pos : 0 }
572
+ }
573
+ }
574
+
575
+ impl < ' a > Iterator for GenresIter < ' a > {
576
+ type Item = & ' a str ;
577
+
578
+ fn next ( & mut self ) -> Option < Self :: Item > {
579
+ if self . pos >= self . value . len ( ) {
580
+ return None ;
581
+ }
582
+
583
+ let remainder = & self . value [ self . pos ..] ;
584
+
585
+ if let Some ( idx) = remainder. find ( V4_MULTI_VALUE_SEPARATOR ) {
586
+ let start = self . pos ;
587
+ let end = self . pos + idx;
588
+ self . pos = end + 1 ;
589
+ return Some ( parse_genre ( & self . value [ start..end] ) ) ;
590
+ }
591
+
592
+ if remainder. starts_with ( '(' ) && remainder. contains ( ')' ) {
593
+ let start = self . pos + 1 ;
594
+ let mut end = self . pos + remainder. find ( ')' ) . unwrap ( ) ;
595
+ self . pos = end + 1 ;
596
+ // handle bracketed refinement e.g. (55)((I think...)"
597
+ if remainder. starts_with ( "((" ) {
598
+ end += 1 ;
599
+ }
600
+ return Some ( parse_genre ( & self . value [ start..end] ) ) ;
601
+ }
602
+
603
+ self . pos = self . value . len ( ) ;
604
+ Some ( parse_genre ( remainder) )
605
+ }
606
+ }
607
+
608
+ fn parse_genre ( genre : & str ) -> & str {
609
+ if genre. len ( ) > 3 {
610
+ return genre;
611
+ }
612
+ if let Ok ( id) = genre. parse :: < usize > ( ) {
613
+ if id < GENRES . len ( ) {
614
+ GENRES [ id]
615
+ } else {
616
+ genre
617
+ }
618
+ } else if genre == "RX" {
619
+ "Remix"
620
+ } else if genre == "CR" {
621
+ "Cover"
622
+ } else {
623
+ genre
624
+ }
625
+ }
626
+
547
627
fn filter_comment_frame_by_description < ' a > (
548
628
frame : & ' a Frame < ' _ > ,
549
629
description : & str ,
@@ -893,6 +973,17 @@ impl SplitTag for Id3v2Tag {
893
973
{
894
974
false // Frame consumed
895
975
} ,
976
+ // TCON needs special treatment to translate genre IDs
977
+ ( "TCON" , FrameValue :: Text ( TextInformationFrame { value : content, .. } ) ) => {
978
+ let genres = GenresIter :: new ( content) ;
979
+ for genre in genres {
980
+ tag. items . push ( TagItem :: new (
981
+ ItemKey :: Genre ,
982
+ ItemValue :: Text ( genre. to_string ( ) ) ,
983
+ ) ) ;
984
+ }
985
+ false // Frame consumed
986
+ } ,
896
987
// Store TXXX/WXXX frames by their descriptions, rather than their IDs
897
988
(
898
989
"TXXX" ,
@@ -1273,7 +1364,7 @@ mod tests {
1273
1364
SplitTag as _, Tag , TagExt as _, TagItem , TagType ,
1274
1365
} ;
1275
1366
1276
- use super :: { COMMENT_FRAME_ID , EMPTY_CONTENT_DESCRIPTOR } ;
1367
+ use super :: { COMMENT_FRAME_ID , EMPTY_CONTENT_DESCRIPTOR , GENRE_ID } ;
1277
1368
1278
1369
fn read_tag ( path : & str ) -> Id3v2Tag {
1279
1370
let tag_bytes = crate :: tag:: utils:: test_utils:: read_path ( path) ;
@@ -2443,4 +2534,69 @@ mod tests {
2443
2534
} ;
2444
2535
assert_eq ! ( url. content, "https://www.myfanpage.com" ) ;
2445
2536
}
2537
+
2538
+ fn id3v2_tag_with_genre ( value : & str ) -> Id3v2Tag {
2539
+ let mut tag = Id3v2Tag :: default ( ) ;
2540
+ let frame = new_text_frame ( GENRE_ID , String :: from ( value) , FrameFlags :: default ( ) ) ;
2541
+ tag. insert ( frame) ;
2542
+ tag
2543
+ }
2544
+
2545
+ #[ test]
2546
+ fn genres_id_multiple ( ) {
2547
+ let tag = id3v2_tag_with_genre ( "(51)(39)" ) ;
2548
+ let mut genres = tag. genres ( ) . unwrap ( ) ;
2549
+ assert_eq ! ( genres. next( ) , Some ( "Techno-Industrial" ) ) ;
2550
+ assert_eq ! ( genres. next( ) , Some ( "Noise" ) ) ;
2551
+ assert_eq ! ( genres. next( ) , None ) ;
2552
+ }
2553
+
2554
+ #[ test]
2555
+ fn genres_id_multiple_into_tag ( ) {
2556
+ let id3v2 = id3v2_tag_with_genre ( "(51)(39)" ) ;
2557
+ let tag: Tag = id3v2. into ( ) ;
2558
+ let mut genres = tag. get_strings ( & ItemKey :: Genre ) ;
2559
+ assert_eq ! ( genres. next( ) , Some ( "Techno-Industrial" ) ) ;
2560
+ assert_eq ! ( genres. next( ) , Some ( "Noise" ) ) ;
2561
+ assert_eq ! ( genres. next( ) , None ) ;
2562
+ }
2563
+
2564
+ #[ test]
2565
+ fn genres_null_separated ( ) {
2566
+ let tag = id3v2_tag_with_genre ( "Samba-rock\0 MPB\0 Funk" ) ;
2567
+ let mut genres = tag. genres ( ) . unwrap ( ) ;
2568
+ assert_eq ! ( genres. next( ) , Some ( "Samba-rock" ) ) ;
2569
+ assert_eq ! ( genres. next( ) , Some ( "MPB" ) ) ;
2570
+ assert_eq ! ( genres. next( ) , Some ( "Funk" ) ) ;
2571
+ assert_eq ! ( genres. next( ) , None ) ;
2572
+ }
2573
+
2574
+ #[ test]
2575
+ fn genres_id_textual_refinement ( ) {
2576
+ let tag = id3v2_tag_with_genre ( "(4)Eurodisco" ) ;
2577
+ let mut genres = tag. genres ( ) . unwrap ( ) ;
2578
+ assert_eq ! ( genres. next( ) , Some ( "Disco" ) ) ;
2579
+ assert_eq ! ( genres. next( ) , Some ( "Eurodisco" ) ) ;
2580
+ assert_eq ! ( genres. next( ) , None ) ;
2581
+ }
2582
+
2583
+ #[ test]
2584
+ fn genres_id_bracketed_refinement ( ) {
2585
+ let tag = id3v2_tag_with_genre ( "(26)(55)((I think...)" ) ;
2586
+ let mut genres = tag. genres ( ) . unwrap ( ) ;
2587
+ assert_eq ! ( genres. next( ) , Some ( "Ambient" ) ) ;
2588
+ assert_eq ! ( genres. next( ) , Some ( "Dream" ) ) ;
2589
+ assert_eq ! ( genres. next( ) , Some ( "(I think...)" ) ) ;
2590
+ assert_eq ! ( genres. next( ) , None ) ;
2591
+ }
2592
+
2593
+ #[ test]
2594
+ fn genres_id_remix_cover ( ) {
2595
+ let tag = id3v2_tag_with_genre ( "(0)(RX)(CR)" ) ;
2596
+ let mut genres = tag. genres ( ) . unwrap ( ) ;
2597
+ assert_eq ! ( genres. next( ) , Some ( "Blues" ) ) ;
2598
+ assert_eq ! ( genres. next( ) , Some ( "Remix" ) ) ;
2599
+ assert_eq ! ( genres. next( ) , Some ( "Cover" ) ) ;
2600
+ assert_eq ! ( genres. next( ) , None ) ;
2601
+ }
2446
2602
}
0 commit comments