diff --git a/flac.go b/flac.go index bd1c831..f9b13e5 100644 --- a/flac.go +++ b/flac.go @@ -6,11 +6,8 @@ package tag import ( "errors" - "fmt" "io" "os" - "strconv" - "strings" ) // BlockType is a type which represents an enumeration of valid FLAC blocks @@ -43,7 +40,7 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { } m := &metadataFLAC{ - c: make(map[string]string), + newMetadataVorbis(), } for { @@ -60,8 +57,7 @@ func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { } type metadataFLAC struct { - c map[string]string // the vorbis comments - p *Picture + *metadataVorbis } func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) { @@ -93,205 +89,6 @@ func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err er return } -func (m *metadataFLAC) readVorbisComment(r io.Reader) error { - vendorLen, err := readInt32LittleEndian(r) - if err != nil { - return err - } - - vendor, err := readString(r, vendorLen) - if err != nil { - return err - } - m.c["vendor"] = vendor - - commentsLen, err := readInt32LittleEndian(r) - if err != nil { - return err - } - - for i := 0; i < commentsLen; i++ { - l, err := readInt32LittleEndian(r) - if err != nil { - return err - } - s, err := readString(r, l) - if err != nil { - return err - } - k, v, err := parseComment(s) - if err != nil { - return err - } - m.c[strings.ToLower(k)] = v - } - return nil -} - -func (m *metadataFLAC) readPictureBlock(r io.Reader) error { - b, err := readInt(r, 4) - if err != nil { - return err - } - pictureType, ok := pictureTypes[byte(b)] - if !ok { - return fmt.Errorf("invalid picture type: %v", b) - } - mimeLen, err := readInt(r, 4) - if err != nil { - return err - } - mime, err := readString(r, mimeLen) - if err != nil { - return err - } - - ext := "" - switch mime { - case "image/jpeg": - ext = "jpg" - case "image/png": - ext = "png" - case "image/gif": - ext = "gif" - } - - descLen, err := readInt(r, 4) - if err != nil { - return err - } - desc, err := readString(r, descLen) - if err != nil { - return err - } - - // We skip width <32>, height <32>, colorDepth <32>, coloresUsed <32> - _, err = readInt(r, 4) // width - if err != nil { - return err - } - _, err = readInt(r, 4) // height - if err != nil { - return err - } - _, err = readInt(r, 4) // color depth - if err != nil { - return err - } - _, err = readInt(r, 4) // colors used - if err != nil { - return err - } - - dataLen, err := readInt(r, 4) - if err != nil { - return err - } - data := make([]byte, dataLen) - _, err = io.ReadFull(r, data) - if err != nil { - return err - } - - m.p = &Picture{ - Ext: ext, - MIMEType: mime, - Type: pictureType, - Description: desc, - Data: data, - } - return nil -} - -func parseComment(c string) (k, v string, err error) { - kv := strings.SplitN(c, "=", 2) - if len(kv) != 2 { - err = errors.New("vorbis comment must contain '='") - return - } - k = kv[0] - v = kv[1] - return -} - -func (m *metadataFLAC) Format() Format { +func (m *metadataFLAC) FileType() FileType { return FLAC } - -func (m *metadataFLAC) Raw() map[string]interface{} { - raw := make(map[string]interface{}, len(m.c)) - for k, v := range m.c { - raw[k] = v - } - return raw -} - -func (m *metadataFLAC) Title() string { - return m.c["title"] -} - -func (m *metadataFLAC) Artist() string { - // PERFORMER - // The artist(s) who performed the work. In classical music this would be the - // conductor, orchestra, soloists. In an audio book it would be the actor who - // did the reading. In popular music this is typically the same as the ARTIST - // and is omitted. - if m.c["performer"] != "" { - return m.c["performer"] - } - return m.c["artist"] -} - -func (m *metadataFLAC) Album() string { - return m.c["album"] -} - -func (m *metadataFLAC) AlbumArtist() string { - // This field isn't included in the standard. - return "" -} - -func (m *metadataFLAC) Composer() string { - // ARTIST - // The artist generally considered responsible for the work. In popular music - // this is usually the performing band or singer. For classical music it would - // be the composer. For an audio book it would be the author of the original text. - if m.c["composer"] != "" { - return m.c["composer"] - } - if m.c["performer"] == "" { - return "" - } - return m.c["artist"] -} - -func (m *metadataFLAC) Genre() string { - return m.c["genre"] -} - -func (m *metadataFLAC) Year() int { - // FIXME: try to parse the date in m.c["date"] to extract this - return 0 -} - -func (m *metadataFLAC) Track() (int, int) { - x, _ := strconv.Atoi(m.c["tracknumber"]) - // https://wiki.xiph.org/Field_names - n, _ := strconv.Atoi(m.c["tracktotal"]) - return x, n -} - -func (m *metadataFLAC) Disc() (int, int) { - // https://wiki.xiph.org/Field_names - x, _ := strconv.Atoi(m.c["discnumber"]) - n, _ := strconv.Atoi(m.c["disctotal"]) - return x, n -} - -func (m *metadataFLAC) Lyrics() string { - return m.c["lyrics"] -} - -func (m *metadataFLAC) Picture() *Picture { - return m.p -} diff --git a/id3v1.go b/id3v1.go index 09bed90..8e66e22 100644 --- a/id3v1.go +++ b/id3v1.go @@ -112,6 +112,7 @@ func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) { type metadataID3v1 map[string]interface{} func (metadataID3v1) Format() Format { return ID3v1 } +func (metadataID3v1) FileType() FileType { return MP3 } func (m metadataID3v1) Raw() map[string]interface{} { return m } func (m metadataID3v1) Title() string { return m["title"].(string) } diff --git a/id3v2metadata.go b/id3v2metadata.go index cf0f122..ff31671 100644 --- a/id3v2metadata.go +++ b/id3v2metadata.go @@ -63,6 +63,7 @@ func (m metadataID3v2) getInt(k string) int { } func (m metadataID3v2) Format() Format { return m.header.Version } +func (m metadataID3v2) FileType() FileType { return MP3 } func (m metadataID3v2) Raw() map[string]interface{} { return m.frames } func (m metadataID3v2) Title() string { diff --git a/mp4.go b/mp4.go index 52279c7..4a4b142 100644 --- a/mp4.go +++ b/mp4.go @@ -151,7 +151,8 @@ func (m metadataMP4) readAtoms(r io.ReadSeeker) error { } } -func (metadataMP4) Format() Format { return MP4 } +func (metadataMP4) Format() Format { return MP4 } +func (metadataMP4) FileType() FileType { return AAC } func (m metadataMP4) Raw() map[string]interface{} { return m } diff --git a/ogg.go b/ogg.go index 328b3ad..f1708c8 100644 --- a/ogg.go +++ b/ogg.go @@ -108,11 +108,18 @@ func ReadOGGTags(r io.ReadSeeker) (Metadata, error) { return nil, err } - m := &metadataFLAC{ - c: make(map[string]string), + m := &metadataOGG{ + newMetadataVorbis(), } err = m.readVorbisComment(r) - return m, err } + +type metadataOGG struct { + *metadataVorbis +} + +func (m *metadataOGG) FileType() FileType { + return OGG +} diff --git a/tag.go b/tag.go index 58ee812..5fdd090 100644 --- a/tag.go +++ b/tag.go @@ -58,7 +58,20 @@ const ( ID3v2_3 = "ID3v2.3" // ID3v2.3 tag format (most common). ID3v2_4 = "ID3v2.4" // ID3v2.4 tag format. MP4 = "MP4" // MP4 tag (atom) format. - FLAC = "FLAC" // FLAC (Vorbis Comment) tag format. + VORBIS = "VORBIS" // Vorbis Comment tag format. +) + +// FileType is an enumeration of the audio file types supported by this package, in particular +// there are audio file types which share metadata formats, and this type is used to distinguish +// between them. +type FileType string + +const ( + MP3 FileType = "MP3" // MP3 file + AAC = "AAC" // M4A file (MP4) + ALAC = "ALAC" // Apple Lossless file FIXME: actually detect this + FLAC = "FLAC" // FLAC file + OGG = "OGG" // OGG file ) // Metadata is an interface which is used to describe metadata retrieved by this package. @@ -66,6 +79,9 @@ type Metadata interface { // Format returns the metadata Format used to encode the data. Format() Format + // FileType returns the file type of the audio file. + FileType() FileType + // Title returns the title of the track. Title() string diff --git a/tag/tag.go b/tag/tag.go index c389a75..ade20dc 100644 --- a/tag/tag.go +++ b/tag/tag.go @@ -61,6 +61,7 @@ func main() { func printMetadata(m tag.Metadata) { fmt.Printf("Metadata Format: %v\n", m.Format()) + fmt.Printf("File Type: %v\n", m.FileType()) fmt.Printf(" Title: %v\n", m.Title()) fmt.Printf(" Album: %v\n", m.Album()) diff --git a/vorbis.go b/vorbis.go new file mode 100644 index 0000000..ba0df56 --- /dev/null +++ b/vorbis.go @@ -0,0 +1,227 @@ +// 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 ( + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +func newMetadataVorbis() *metadataVorbis { + return &metadataVorbis{ + c: make(map[string]string), + } +} + +type metadataVorbis struct { + c map[string]string // the vorbis comments + p *Picture +} + +func (m *metadataVorbis) readVorbisComment(r io.Reader) error { + vendorLen, err := readInt32LittleEndian(r) + if err != nil { + return err + } + + vendor, err := readString(r, vendorLen) + if err != nil { + return err + } + m.c["vendor"] = vendor + + commentsLen, err := readInt32LittleEndian(r) + if err != nil { + return err + } + + for i := 0; i < commentsLen; i++ { + l, err := readInt32LittleEndian(r) + if err != nil { + return err + } + s, err := readString(r, l) + if err != nil { + return err + } + k, v, err := parseComment(s) + if err != nil { + return err + } + m.c[strings.ToLower(k)] = v + } + return nil +} + +func (m *metadataVorbis) readPictureBlock(r io.Reader) error { + b, err := readInt(r, 4) + if err != nil { + return err + } + pictureType, ok := pictureTypes[byte(b)] + if !ok { + return fmt.Errorf("invalid picture type: %v", b) + } + mimeLen, err := readInt(r, 4) + if err != nil { + return err + } + mime, err := readString(r, mimeLen) + if err != nil { + return err + } + + ext := "" + switch mime { + case "image/jpeg": + ext = "jpg" + case "image/png": + ext = "png" + case "image/gif": + ext = "gif" + } + + descLen, err := readInt(r, 4) + if err != nil { + return err + } + desc, err := readString(r, descLen) + if err != nil { + return err + } + + // We skip width <32>, height <32>, colorDepth <32>, coloresUsed <32> + _, err = readInt(r, 4) // width + if err != nil { + return err + } + _, err = readInt(r, 4) // height + if err != nil { + return err + } + _, err = readInt(r, 4) // color depth + if err != nil { + return err + } + _, err = readInt(r, 4) // colors used + if err != nil { + return err + } + + dataLen, err := readInt(r, 4) + if err != nil { + return err + } + data := make([]byte, dataLen) + _, err = io.ReadFull(r, data) + if err != nil { + return err + } + + m.p = &Picture{ + Ext: ext, + MIMEType: mime, + Type: pictureType, + Description: desc, + Data: data, + } + return nil +} + +func parseComment(c string) (k, v string, err error) { + kv := strings.SplitN(c, "=", 2) + if len(kv) != 2 { + err = errors.New("vorbis comment must contain '='") + return + } + k = kv[0] + v = kv[1] + return +} + +func (m *metadataVorbis) Format() Format { + return FLAC +} + +func (m *metadataVorbis) Raw() map[string]interface{} { + raw := make(map[string]interface{}, len(m.c)) + for k, v := range m.c { + raw[k] = v + } + return raw +} + +func (m *metadataVorbis) Title() string { + return m.c["title"] +} + +func (m *metadataVorbis) Artist() string { + // PERFORMER + // The artist(s) who performed the work. In classical music this would be the + // conductor, orchestra, soloists. In an audio book it would be the actor who + // did the reading. In popular music this is typically the same as the ARTIST + // and is omitted. + if m.c["performer"] != "" { + return m.c["performer"] + } + return m.c["artist"] +} + +func (m *metadataVorbis) Album() string { + return m.c["album"] +} + +func (m *metadataVorbis) AlbumArtist() string { + // This field isn't included in the standard. + return "" +} + +func (m *metadataVorbis) Composer() string { + // ARTIST + // The artist generally considered responsible for the work. In popular music + // this is usually the performing band or singer. For classical music it would + // be the composer. For an audio book it would be the author of the original text. + if m.c["composer"] != "" { + return m.c["composer"] + } + if m.c["performer"] == "" { + return "" + } + return m.c["artist"] +} + +func (m *metadataVorbis) Genre() string { + return m.c["genre"] +} + +func (m *metadataVorbis) Year() int { + // FIXME: try to parse the date in m.c["date"] to extract this + return 0 +} + +func (m *metadataVorbis) Track() (int, int) { + x, _ := strconv.Atoi(m.c["tracknumber"]) + // https://wiki.xiph.org/Field_names + n, _ := strconv.Atoi(m.c["tracktotal"]) + return x, n +} + +func (m *metadataVorbis) Disc() (int, int) { + // https://wiki.xiph.org/Field_names + x, _ := strconv.Atoi(m.c["discnumber"]) + n, _ := strconv.Atoi(m.c["disctotal"]) + return x, n +} + +func (m *metadataVorbis) Lyrics() string { + return m.c["lyrics"] +} + +func (m *metadataVorbis) Picture() *Picture { + return m.p +}