145 lines
3.3 KiB
Go
145 lines
3.3 KiB
Go
|
// Minimal 'Vorbis comment' metadata reader oriented around
|
||
|
// https://xiph.org/vorbis/doc/Vorbis_I_spec.html and its reference
|
||
|
// implementation written in C.
|
||
|
package vorbis
|
||
|
|
||
|
import (
|
||
|
"encoding/binary"
|
||
|
"errors"
|
||
|
"io"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrVorbisHeaderType = errors.New("vorbis: header not Vorbis")
|
||
|
ErrVorbisInvalidCommentFormat = errors.New("vorbis: invalid Vorbis comment")
|
||
|
)
|
||
|
|
||
|
type VorbisCommentField struct {
|
||
|
// According to the spec, the key capitalization doesn't matter, which is
|
||
|
// why we're using uppercase letters only in this implementation
|
||
|
// (see `VorbisCommentDecode()`).
|
||
|
Key string
|
||
|
// The value has some restrictions for what characters it can contain, but
|
||
|
// we're ignoring them for now.
|
||
|
Val string
|
||
|
}
|
||
|
|
||
|
// Track metadata represented by a Vorbis comment. The fields should be
|
||
|
// self-explanatory.
|
||
|
type VorbisComment struct {
|
||
|
Vendor string
|
||
|
Fields []VorbisCommentField
|
||
|
}
|
||
|
|
||
|
// Attempts to decode a Vorbis comment, leaving the reader `r` right past the
|
||
|
// data it decoded.
|
||
|
func VorbisCommentDecode(r io.Reader) (VorbisComment, error) {
|
||
|
var ret VorbisComment
|
||
|
|
||
|
// In Vorbis comment, strings are always preceded by a 32-bit length
|
||
|
// specifier.
|
||
|
getNextString := func() ([]byte, error) {
|
||
|
var sz uint32
|
||
|
err := binary.Read(r, binary.LittleEndian, &sz)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
content := make([]byte, sz)
|
||
|
_, err = r.Read(content)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
return content, nil
|
||
|
}
|
||
|
|
||
|
content, err := getNextString()
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
ret.Vendor = string(content)
|
||
|
|
||
|
var numCommentFields uint32
|
||
|
err = binary.Read(r, binary.LittleEndian, &numCommentFields)
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
|
||
|
for i := 0; i < int(numCommentFields); i++ {
|
||
|
content, err := getNextString()
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
|
||
|
splits := strings.Split(string(content), "=")
|
||
|
if len(splits) != 2 {
|
||
|
return ret, ErrVorbisInvalidCommentFormat
|
||
|
}
|
||
|
|
||
|
var newField VorbisCommentField
|
||
|
|
||
|
newField.Key = strings.ToUpper(splits[0])
|
||
|
newField.Val = splits[1]
|
||
|
|
||
|
ret.Fields = append(ret.Fields, newField)
|
||
|
}
|
||
|
return ret, nil
|
||
|
}
|
||
|
|
||
|
// Field names are searched case insensitively, as specified in the spec.
|
||
|
// `found` is set to false if the field doesn't exist.
|
||
|
func (c *VorbisComment) FieldByName(name string) (val string, found bool) {
|
||
|
// All field names are stored as upper case strings in this implementation.
|
||
|
// That is why we only need to transform the search query string to
|
||
|
// uppercase.
|
||
|
upperName := strings.ToUpper(name)
|
||
|
// Linearly search through field names.
|
||
|
for _, v := range c.Fields {
|
||
|
if v.Key == upperName {
|
||
|
return v.Val, true
|
||
|
}
|
||
|
}
|
||
|
return "", false
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
PackTypeInfo = uint8(0x1)
|
||
|
PackTypeComment = uint8(0x3) // Comment is the only one we really care about here.
|
||
|
PackTypeBooks = uint8(0x5)
|
||
|
)
|
||
|
|
||
|
type VorbisHeader struct {
|
||
|
PackType uint8
|
||
|
Comment *VorbisComment
|
||
|
}
|
||
|
|
||
|
func VorbisHeaderDecode(r io.Reader) (VorbisHeader, error) {
|
||
|
var ret VorbisHeader
|
||
|
|
||
|
err := binary.Read(r, binary.LittleEndian, &ret.PackType)
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
|
||
|
switch ret.PackType {
|
||
|
case PackTypeComment:
|
||
|
buf := make([]byte, 6)
|
||
|
_, err = r.Read(buf)
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
if string(buf) != "vorbis" {
|
||
|
return ret, ErrVorbisHeaderType
|
||
|
}
|
||
|
|
||
|
comment, err := VorbisCommentDecode(r)
|
||
|
if err != nil {
|
||
|
return ret, err
|
||
|
}
|
||
|
ret.Comment = &comment
|
||
|
}
|
||
|
return ret, nil
|
||
|
}
|