Skip to content

Commit f1afc3e

Browse files
sublipriSerial-ATA
authored andcommitted
ID3v2: Add genres method to Id3v2Tag
1 parent 23c334e commit f1afc3e

File tree

2 files changed

+158
-1
lines changed

2 files changed

+158
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
### Added
1010
- **ParseOptions**: `ParseOptions::allocation_limit` to change the default allocation limit. ([PR](https://github.com/Serial-ATA/lofty-rs/pull/276))
11+
- **ID3v2**: `Id3v2Tag::genres` to handle all the ways genres can be stored in `TCON` frames. ([PR](https://github.com/Serial-ATA/lofty-rs/pull/286))
1112

1213
### Changed
1314
- **VorbisComments**: When converting from `Tag` to `VorbisComments`, `ItemKey::Unknown`s will be checked for spec compliance. ([PR](https://github.com/Serial-ATA/lofty-rs/pull/272))

src/id3/v2/tag.rs

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use super::frame::id::FrameId;
22
use super::frame::{Frame, FrameFlags, FrameValue, EMPTY_CONTENT_DESCRIPTOR, UNKNOWN_LANGUAGE};
33
use super::header::{Id3v2TagFlags, Id3v2Version};
44
use crate::error::{LoftyError, Result};
5+
use crate::id3::v1::GENRES;
56
use crate::id3::v2::frame::{FrameRef, MUSICBRAINZ_UFID_OWNER};
67
use crate::id3::v2::items::{
78
AttachedPictureFrame, CommentFrame, ExtendedTextFrame, ExtendedUrlFrame, TextInformationFrame,
@@ -530,6 +531,22 @@ impl Id3v2Tag {
530531
};
531532
}
532533

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+
533550
fn insert_number_pair(
534551
&mut self,
535552
id: FrameId<'static>,
@@ -544,6 +561,69 @@ impl Id3v2Tag {
544561
}
545562
}
546563

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+
547627
fn filter_comment_frame_by_description<'a>(
548628
frame: &'a Frame<'_>,
549629
description: &str,
@@ -893,6 +973,17 @@ impl SplitTag for Id3v2Tag {
893973
{
894974
false // Frame consumed
895975
},
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+
},
896987
// Store TXXX/WXXX frames by their descriptions, rather than their IDs
897988
(
898989
"TXXX",
@@ -1273,7 +1364,7 @@ mod tests {
12731364
SplitTag as _, Tag, TagExt as _, TagItem, TagType,
12741365
};
12751366

1276-
use super::{COMMENT_FRAME_ID, EMPTY_CONTENT_DESCRIPTOR};
1367+
use super::{COMMENT_FRAME_ID, EMPTY_CONTENT_DESCRIPTOR, GENRE_ID};
12771368

12781369
fn read_tag(path: &str) -> Id3v2Tag {
12791370
let tag_bytes = crate::tag::utils::test_utils::read_path(path);
@@ -2443,4 +2534,69 @@ mod tests {
24432534
};
24442535
assert_eq!(url.content, "https://www.myfanpage.com");
24452536
}
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\0MPB\0Funk");
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+
}
24462602
}

0 commit comments

Comments
 (0)