Reorganised Vorbis Comment parsing into own type
- Added FileType to Metadata to distinguish between FLAC and OGG - TODO: fix ALAC files being identified as AAC
This commit is contained in:
parent
77d9aa6414
commit
4c2b34fa94
209
flac.go
209
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
|
||||
}
|
||||
|
1
id3v1.go
1
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) }
|
||||
|
@ -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 {
|
||||
|
3
mp4.go
3
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 }
|
||||
|
||||
|
13
ogg.go
13
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
|
||||
}
|
||||
|
18
tag.go
18
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
|
||||
|
||||
|
@ -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())
|
||||
|
227
vorbis.go
Normal file
227
vorbis.go
Normal file
@ -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
|
||||
}
|
Loading…
Reference in New Issue
Block a user