diff --git a/flac.go b/flac.go new file mode 100644 index 0000000..bc4780e --- /dev/null +++ b/flac.go @@ -0,0 +1,223 @@ +// 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 ( + "bytes" + "errors" + "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 +} + +func (m *metadataFLAC) readFLACMetadataBlock(rs io.ReadSeeker) (last bool, err error) { + blockHeader, err := readBytes(rs, 1) + if err != nil { + log.Println(err) + return + } + + if getBit(blockHeader[0], 7) { + blockHeader[0] ^= (1 << 7) + last = true + } + + blockLen, err := readInt(rs, 3) + if err != nil { + return + } + + if BlockType(blockHeader[0]) != VorbisCommentBlock { + _, err = rs.Seek(int64(blockLen), os.SEEK_CUR) + return + } + + commentBytes := make([]byte, blockLen) + _, err = rs.Read(commentBytes) + if err != nil { + return + } + + r := bytes.NewReader(commentBytes) + m.readVorbisComment(r) + 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 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 +} diff --git a/tag.go b/tag.go index 216fd42..fe37411 100644 --- a/tag.go +++ b/tag.go @@ -26,6 +26,8 @@ func ReadFrom(r io.ReadSeeker) (Metadata, error) { } switch { + case string(b[0:4]) == "fLaC": + return ReadFLACTags(r) case string(b[4:11]) == "ftypM4A": return ReadAtoms(r) @@ -53,6 +55,7 @@ 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. ) // Metadata is an interface which is used to describe metadata retrieved by this package. diff --git a/util.go b/util.go index 5087ce8..ec28ac6 100644 --- a/util.go +++ b/util.go @@ -5,6 +5,7 @@ package tag import ( + "encoding/binary" "io" ) @@ -63,3 +64,9 @@ func read7BitChunkedInt(r io.Reader, n int) (int, error) { } return get7BitChunkedInt(b), nil } + +func readInt32LittleEndian(r io.Reader) (int, error) { + var n int32 + err := binary.Read(r, binary.LittleEndian, &n) + return int(n), err +}