From 1e646522d630636c61e0495be685a5e2aed74c2b Mon Sep 17 00:00:00 2001 From: Xavier Henner Date: Sat, 4 Jul 2015 14:17:53 +0200 Subject: [PATCH] Support for numeric genres in id3v2 TCON The 'Content type', which previously was stored as a one byte numeric value only, is now a numeric string. You may use one or several of the types as ID3v1.1 did or, since the category list would be impossible to maintain with accurate and up to date categories, define your own. References to the ID3v1 genres can be made by, as first byte, enter "(" followed by a number from the genres list (appendix A) and ended with a ")" character. This is optionally followed by a refinement, e.g. "(21)" or "(4)Eurodisco". Several references can be made in the same frame, e.g. "(51)(39)". If the refinement should begin with a "(" character it should be replaced with "((", e.g. "((I can figure out any genre)" or "(55)((I think...)". The following new content types is defined in ID3v2 and is implemented in the same way as the numerig content types, e.g. "(RX)". To test it, use the id3v2 tool % id3v2 -g 79 test.mp3 % id3v2 -l test.mp3| grep TCON TCON (Content type): Hard Rock (79) % ./tag test.mp3| grep Genre Genre: (79) With the patch : % go build && ./tag test.mp3| grep Genre Genre: Hard Rock --- id3v2.go | 56 ++++++++++++++++++++++++++++++++++++++++++++++++ id3v2_test.go | 21 ++++++++++++++++++ id3v2metadata.go | 2 +- 3 files changed, 78 insertions(+), 1 deletion(-) diff --git a/id3v2.go b/id3v2.go index c6f406d..bf1b20f 100644 --- a/id3v2.go +++ b/id3v2.go @@ -7,9 +7,41 @@ package tag import ( "fmt" "io" + "regexp" "strconv" + "strings" ) +var id3v2Genres = [...]string{ + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", + "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", + "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", + "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", + "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", + "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer", + "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", + "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", + "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", + "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", + "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", + "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", + "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", + "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", + "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", + "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", + "Synthpop", +} + // id3v2Header is a type which represents an ID3v2 tag header. type id3v2Header struct { Version Format @@ -342,3 +374,27 @@ func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) { } return metadataID3v2{header: h, frames: f}, nil } + +// id3v2genre parse a id3v2 genre tag and expand the numeric genres +func id3v2genre(genre string) string { + c := true + for c { + orig := genre + re := regexp.MustCompile("(.*[^(]|.* |^)\\(([0-9]+)\\) *(.*)$") + if match := re.FindStringSubmatch(genre); len(match) > 0 { + if genreId, err := strconv.Atoi(match[2]); err == nil { + if genreId < len(id3v2Genres) { + genre = id3v2Genres[genreId] + if match[1] != "" { + genre = strings.TrimSpace(match[1]) + " " + genre + } + if match[3] != "" { + genre = genre + " " + match[3] + } + } + } + } + c = (orig != genre) + } + return strings.Replace(genre, "((", "(", -1) +} diff --git a/id3v2_test.go b/id3v2_test.go index 03e6035..4630409 100644 --- a/id3v2_test.go +++ b/id3v2_test.go @@ -129,3 +129,24 @@ func TestUnsynchroniserSplitReads(t *testing.T) { } } } + +func TestGenreExpension(t *testing.T) { + var tests = map[string]string{ + "Test": "Test", + "((17)": "(17)", + "(17) Test": "Rock Test", + "(17)Test": "Rock Test", + "(17)": "Rock", + "Test(17)": "Test Rock", + "Test (17)": "Test Rock", + "(17)(93)": "Rock Psychedelic Rock", + "(17)Test(93)": "Rock Test Psychedelic Rock", + } + for g, r := range tests { + got := id3v2genre(g) + + if got != r { + t.Errorf("[%v] got: %v, expected %v", g, got, r) + } + } +} diff --git a/id3v2metadata.go b/id3v2metadata.go index 6573a3b..9662efc 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -87,7 +87,7 @@ func (m metadataID3v2) Composer() string { } func (m metadataID3v2) Genre() string { - return m.getString(frames.Name("genre", m.Format())) + return id3v2genre(m.getString(frames.Name("genre", m.Format()))) } func (m metadataID3v2) Year() int {