2015-03-19 13:21:53 +01:00
|
|
|
// Copyright 2015, David Howden
|
|
|
|
// Use of this source code is governed by a BSD-style
|
|
|
|
// license that can be found in the LICENSE file.
|
|
|
|
|
|
|
|
package tag
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"io"
|
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
2015-07-04 14:17:53 +02:00
|
|
|
"regexp"
|
2015-04-26 17:55:59 +02:00
|
|
|
"strconv"
|
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
2015-07-04 14:17:53 +02:00
|
|
|
"strings"
|
2015-03-19 13:21:53 +01:00
|
|
|
)
|
|
|
|
|
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
2015-07-04 14:17:53 +02:00
|
|
|
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",
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
// id3v2Header is a type which represents an ID3v2 tag header.
|
|
|
|
type id3v2Header struct {
|
2015-03-19 13:21:53 +01:00
|
|
|
Version Format
|
|
|
|
Unsynchronisation bool
|
|
|
|
ExtendedHeader bool
|
|
|
|
Experimental bool
|
|
|
|
Size int
|
|
|
|
}
|
|
|
|
|
|
|
|
// readID3v2Header reads the ID3v2 header from the given io.Reader.
|
2015-06-28 04:40:49 +02:00
|
|
|
func readID3v2Header(r io.Reader) (*id3v2Header, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
b, err := readBytes(r, 10)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if string(b[0:3]) != "ID3" {
|
|
|
|
return nil, fmt.Errorf("expected to read \"ID3\"")
|
|
|
|
}
|
|
|
|
|
|
|
|
b = b[3:]
|
|
|
|
var vers Format
|
|
|
|
switch uint(b[0]) {
|
|
|
|
case 2:
|
|
|
|
vers = ID3v2_2
|
|
|
|
case 3:
|
|
|
|
vers = ID3v2_3
|
|
|
|
case 4:
|
|
|
|
vers = ID3v2_4
|
|
|
|
case 0, 1:
|
|
|
|
fallthrough
|
|
|
|
default:
|
|
|
|
return nil, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
|
|
|
|
}
|
|
|
|
|
|
|
|
// NB: We ignore b[1] (the revision) as we don't currently rely on it.
|
2015-06-28 04:40:49 +02:00
|
|
|
return &id3v2Header{
|
2015-03-19 13:21:53 +01:00
|
|
|
Version: vers,
|
|
|
|
Unsynchronisation: getBit(b[2], 7),
|
|
|
|
ExtendedHeader: getBit(b[2], 6),
|
|
|
|
Experimental: getBit(b[2], 5),
|
|
|
|
Size: get7BitChunkedInt(b[3:7]),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
// id3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame.
|
|
|
|
type id3v2FrameFlags struct {
|
2015-06-28 04:34:42 +02:00
|
|
|
// Message (ID3 2.3.0 and 2.4.0)
|
2015-03-19 13:21:53 +01:00
|
|
|
TagAlterPreservation bool
|
|
|
|
FileAlterPreservation bool
|
|
|
|
ReadOnly bool
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
// Format (ID3 2.3.0 and 2.4.0)
|
|
|
|
Compression bool
|
|
|
|
Encryption bool
|
|
|
|
GroupIdentity bool
|
|
|
|
// ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1)
|
2015-03-19 13:21:53 +01:00
|
|
|
Unsynchronisation bool
|
|
|
|
DataLengthIndicator bool
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
func readID3v23FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
|
2015-06-28 04:34:42 +02:00
|
|
|
b, err := readBytes(r, 2)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
msg := b[0]
|
|
|
|
fmt := b[1]
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
return &id3v2FrameFlags{
|
2015-06-28 04:34:42 +02:00
|
|
|
TagAlterPreservation: getBit(msg, 7),
|
|
|
|
FileAlterPreservation: getBit(msg, 6),
|
|
|
|
ReadOnly: getBit(msg, 5),
|
|
|
|
Compression: getBit(fmt, 7),
|
|
|
|
Encryption: getBit(fmt, 6),
|
|
|
|
GroupIdentity: getBit(fmt, 5),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
func readID3v24FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
b, err := readBytes(r, 2)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
msg := b[0]
|
|
|
|
fmt := b[1]
|
|
|
|
|
2015-06-28 04:40:49 +02:00
|
|
|
return &id3v2FrameFlags{
|
2015-03-19 13:21:53 +01:00
|
|
|
TagAlterPreservation: getBit(msg, 6),
|
|
|
|
FileAlterPreservation: getBit(msg, 5),
|
|
|
|
ReadOnly: getBit(msg, 4),
|
2015-06-28 04:34:42 +02:00
|
|
|
GroupIdentity: getBit(fmt, 6),
|
2015-03-19 13:21:53 +01:00
|
|
|
Compression: getBit(fmt, 3),
|
|
|
|
Encryption: getBit(fmt, 2),
|
|
|
|
Unsynchronisation: getBit(fmt, 1),
|
|
|
|
DataLengthIndicator: getBit(fmt, 0),
|
|
|
|
}, nil
|
2015-06-28 04:34:42 +02:00
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func readID3v2_2FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
|
|
|
name, err = readString(r, 3)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
size, err = readInt(r, 3)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
headerSize = 6
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func readID3v2_3FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
|
|
|
name, err = readString(r, 4)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
size, err = readInt(r, 4)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
headerSize = 8
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func readID3v2_4FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
|
|
|
|
name, err = readString(r, 4)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
size, err = read7BitChunkedInt(r, 4)
|
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
headerSize = 8
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header.
|
2015-06-28 04:40:49 +02:00
|
|
|
func readID3v2Frames(r io.Reader, h *id3v2Header) (map[string]interface{}, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
offset := 10 // the size of the header
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
|
|
|
|
for offset < h.Size {
|
|
|
|
var err error
|
|
|
|
var name string
|
|
|
|
var size, headerSize int
|
2015-06-28 04:40:49 +02:00
|
|
|
var flags *id3v2FrameFlags
|
2015-03-19 13:21:53 +01:00
|
|
|
|
|
|
|
switch h.Version {
|
|
|
|
case ID3v2_2:
|
|
|
|
name, size, headerSize, err = readID3v2_2FrameHeader(r)
|
|
|
|
|
|
|
|
case ID3v2_3:
|
|
|
|
name, size, headerSize, err = readID3v2_3FrameHeader(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-06-28 04:34:42 +02:00
|
|
|
flags, err = readID3v23FrameFlags(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
headerSize += 2
|
|
|
|
|
|
|
|
case ID3v2_4:
|
|
|
|
name, size, headerSize, err = readID3v2_4FrameHeader(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-06-28 04:34:42 +02:00
|
|
|
flags, err = readID3v24FrameFlags(r)
|
2015-03-19 13:21:53 +01:00
|
|
|
headerSize += 2
|
|
|
|
}
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-26 17:55:59 +02:00
|
|
|
|
2015-06-28 01:53:07 +02:00
|
|
|
// FIXME: Do we still need this?
|
2015-04-26 16:56:26 +02:00
|
|
|
// if size=0, we certainly are in a padding zone. ignore the rest of
|
2015-04-30 22:07:08 +02:00
|
|
|
// the tags
|
|
|
|
if size == 0 {
|
2015-04-26 16:56:26 +02:00
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
offset += headerSize + size
|
|
|
|
|
2015-07-05 23:03:02 +02:00
|
|
|
// Avoid corrupted padding (see http://id3.org/Compliance%20Issues).
|
|
|
|
if !validID3Frame(h.Version, name) && offset > h.Size {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
if flags != nil {
|
|
|
|
if flags.Compression {
|
|
|
|
_, err = read7BitChunkedInt(r, 4) // read 4
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
size -= 4
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-06-28 04:34:42 +02:00
|
|
|
if flags.Encryption {
|
|
|
|
_, err = readBytes(r, 1) // read 1 byte of encryption method
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
size -= 1
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
b, err := readBytes(r, size)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// There can be multiple tag with the same name. Append a number to the
|
|
|
|
// name if there is more than one.
|
|
|
|
rawName := name
|
|
|
|
if _, ok := result[rawName]; ok {
|
|
|
|
for i := 0; ok; i++ {
|
|
|
|
rawName = name + "_" + strconv.Itoa(i)
|
|
|
|
_, ok = result[rawName]
|
|
|
|
}
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
switch {
|
2015-05-22 00:59:20 +02:00
|
|
|
case name == "TXXX" || name == "TXX":
|
|
|
|
t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-03-19 13:21:53 +01:00
|
|
|
case name[0] == 'T':
|
2015-05-24 04:20:16 +02:00
|
|
|
txt, err := readTFrame(b)
|
2015-05-22 00:59:20 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = txt
|
|
|
|
|
2015-05-22 01:20:55 +02:00
|
|
|
case name == "UFID" || name == "UFI":
|
2015-05-24 04:11:52 +02:00
|
|
|
t, err := readUFID(b)
|
2015-05-22 01:20:55 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-05-22 00:59:20 +02:00
|
|
|
case name == "WXXX" || name == "WXX":
|
|
|
|
t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
|
|
|
case name[0] == 'W':
|
2015-05-24 04:20:16 +02:00
|
|
|
txt, err := readWFrame(b)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = txt
|
2015-03-19 13:21:53 +01:00
|
|
|
|
2015-05-18 09:32:54 +02:00
|
|
|
case name == "COMM" || name == "USLT":
|
2015-05-22 00:59:20 +02:00
|
|
|
t, err := readTextWithDescrFrame(b, true, true) // both lang and enc
|
2015-05-18 09:32:54 +02:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
result[rawName] = t
|
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
case name == "APIC":
|
2015-03-19 13:21:53 +01:00
|
|
|
p, err := readAPICFrame(b)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = p
|
2015-03-19 13:21:53 +01:00
|
|
|
|
2015-04-27 15:11:04 +02:00
|
|
|
case name == "PIC":
|
2015-03-19 13:21:53 +01:00
|
|
|
p, err := readPICFrame(b)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-27 15:11:04 +02:00
|
|
|
result[rawName] = p
|
2015-06-28 01:53:57 +02:00
|
|
|
|
2015-05-24 21:19:15 +02:00
|
|
|
default:
|
|
|
|
result[rawName] = b
|
2015-03-19 13:21:53 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
return result, nil
|
|
|
|
}
|
|
|
|
|
2015-05-02 01:58:05 +02:00
|
|
|
type unsynchroniser struct {
|
|
|
|
io.Reader
|
|
|
|
ff bool
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// filter io.Reader which skip the Unsynchronisation bytes
|
2015-05-03 01:58:08 +02:00
|
|
|
func (r *unsynchroniser) Read(p []byte) (int, error) {
|
2015-05-02 01:58:05 +02:00
|
|
|
b := make([]byte, 1)
|
2015-05-03 01:58:08 +02:00
|
|
|
i := 0
|
2015-05-02 01:58:05 +02:00
|
|
|
for i < len(p) {
|
2015-05-03 01:58:08 +02:00
|
|
|
if n, err := r.Reader.Read(b); err != nil || n == 0 {
|
2015-04-26 16:56:26 +02:00
|
|
|
return i, err
|
|
|
|
}
|
2015-05-02 01:58:05 +02:00
|
|
|
if r.ff && b[0] == 0x00 {
|
|
|
|
r.ff = false
|
2015-04-30 22:07:08 +02:00
|
|
|
continue
|
|
|
|
}
|
2015-05-02 01:58:05 +02:00
|
|
|
p[i] = b[0]
|
2015-04-30 22:07:08 +02:00
|
|
|
i++
|
2015-05-02 01:58:05 +02:00
|
|
|
r.ff = (b[0] == 0xFF)
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
2015-05-03 01:58:08 +02:00
|
|
|
return i, nil
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
2015-04-14 16:06:32 +02:00
|
|
|
// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
|
2015-03-19 13:21:53 +01:00
|
|
|
// non-nil error on failure.
|
2015-04-14 16:06:32 +02:00
|
|
|
func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
|
2015-03-19 13:21:53 +01:00
|
|
|
h, err := readID3v2Header(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2015-04-26 16:56:26 +02:00
|
|
|
|
|
|
|
var ur io.Reader
|
2015-05-02 01:58:05 +02:00
|
|
|
ur = r
|
2015-04-26 16:56:26 +02:00
|
|
|
if h.Unsynchronisation {
|
2015-05-02 01:58:05 +02:00
|
|
|
ur = &unsynchroniser{Reader: r}
|
2015-04-26 16:56:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
f, err := readID3v2Frames(ur, h)
|
2015-03-19 13:21:53 +01:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return metadataID3v2{header: h, frames: f}, nil
|
|
|
|
}
|
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
2015-07-04 14:17:53 +02:00
|
|
|
|
|
|
|
// 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)
|
|
|
|
}
|