2015-04-14 16:09:58 +02: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 (
|
|
|
|
"errors"
|
2015-04-15 14:20:10 +02:00
|
|
|
"fmt"
|
2015-04-14 16:09:58 +02:00
|
|
|
"io"
|
|
|
|
"log"
|
|
|
|
"os"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
)
|
|
|
|
|
|
|
|
// BlockType is a type which represents an enumeration of valid FLAC blocks
|
|
|
|
type BlockType byte
|
|
|
|
|
|
|
|
const (
|
|
|
|
StreamInfoBlock BlockType = 0
|
|
|
|
PaddingBlock = 1
|
|
|
|
ApplicationBlock = 2
|
|
|
|
SeektableBlock = 3
|
|
|
|
VorbisCommentBlock = 4 // Supported
|
|
|
|
CueSheetBlock = 5
|
|
|
|
PictureBlock = 6 // Supported
|
|
|
|
)
|
|
|
|
|
|
|
|
// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting
|
|
|
|
// metadata in a Metadata implementation, or non-nil error if there was a problem.
|
|
|
|
func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
|
|
|
|
_, err := r.Seek(0, os.SEEK_SET)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
flac, err := readString(r, 4)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if flac != "fLaC" {
|
|
|
|
return nil, errors.New("expected 'fLaC'")
|
|
|
|
}
|
|
|
|
|
|
|
|
m := &metadataFLAC{
|
|
|
|
c: make(map[string]string),
|
|
|
|
}
|
|
|
|
|
|
|
|
for {
|
|
|
|
last, err := m.readFLACMetadataBlock(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if last {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type metadataFLAC struct {
|
|
|
|
c map[string]string // the vorbis comments
|
|
|
|
p *Picture
|
|
|
|
}
|
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
|
|
|
|
blockHeader, err := readBytes(r, 1)
|
2015-04-14 16:09:58 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Println(err)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
if getBit(blockHeader[0], 7) {
|
|
|
|
blockHeader[0] ^= (1 << 7)
|
|
|
|
last = true
|
|
|
|
}
|
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
blockLen, err := readInt(r, 3)
|
2015-04-14 16:09:58 +02:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
switch BlockType(blockHeader[0]) {
|
|
|
|
case VorbisCommentBlock:
|
|
|
|
err = m.readVorbisComment(r)
|
2015-04-14 16:09:58 +02:00
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
case PictureBlock:
|
|
|
|
err = m.readPictureBlock(r)
|
2015-04-14 16:09:58 +02:00
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
default:
|
|
|
|
_, err = r.Seek(int64(blockLen), os.SEEK_CUR)
|
|
|
|
}
|
2015-04-14 16:09:58 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-04-15 14:20:10 +02:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2015-04-14 16:09:58 +02:00
|
|
|
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 {
|
|
|
|
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) Picture() *Picture {
|
|
|
|
return m.p
|
|
|
|
}
|