radio-stream-recorder/vorbis/metadata.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
}