summaryrefslogtreecommitdiff
path: root/vendor/github.com/dhowden
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/dhowden')
-rw-r--r--vendor/github.com/dhowden/tag/.editorconfig19
-rw-r--r--vendor/github.com/dhowden/tag/.travis.yml5
-rw-r--r--vendor/github.com/dhowden/tag/LICENSE23
-rw-r--r--vendor/github.com/dhowden/tag/README.md72
-rw-r--r--vendor/github.com/dhowden/tag/dsf.go110
-rw-r--r--vendor/github.com/dhowden/tag/flac.go89
-rw-r--r--vendor/github.com/dhowden/tag/id.go81
-rw-r--r--vendor/github.com/dhowden/tag/id3v1.go144
-rw-r--r--vendor/github.com/dhowden/tag/id3v2.go434
-rw-r--r--vendor/github.com/dhowden/tag/id3v2frames.go638
-rw-r--r--vendor/github.com/dhowden/tag/id3v2metadata.go141
-rw-r--r--vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt3
-rw-r--r--vendor/github.com/dhowden/tag/mp4.go372
-rw-r--r--vendor/github.com/dhowden/tag/ogg.go119
-rw-r--r--vendor/github.com/dhowden/tag/sum.go219
-rw-r--r--vendor/github.com/dhowden/tag/tag.go147
-rw-r--r--vendor/github.com/dhowden/tag/util.go81
-rw-r--r--vendor/github.com/dhowden/tag/vorbis.go255
18 files changed, 2952 insertions, 0 deletions
diff --git a/vendor/github.com/dhowden/tag/.editorconfig b/vendor/github.com/dhowden/tag/.editorconfig
new file mode 100644
index 0000000..57515d0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*.go]
+indent_style = tab
+indent_size = 3
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*]
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/vendor/github.com/dhowden/tag/.travis.yml b/vendor/github.com/dhowden/tag/.travis.yml
new file mode 100644
index 0000000..3a81bf9
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.travis.yml
@@ -0,0 +1,5 @@
+language: go
+
+go:
+ - 1.7
+ - tip \ No newline at end of file
diff --git a/vendor/github.com/dhowden/tag/LICENSE b/vendor/github.com/dhowden/tag/LICENSE
new file mode 100644
index 0000000..dfd760c
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2015, David Howden
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/dhowden/tag/README.md b/vendor/github.com/dhowden/tag/README.md
new file mode 100644
index 0000000..de81883
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/README.md
@@ -0,0 +1,72 @@
+# MP3/MP4/OGG/FLAC metadata parsing library
+[![Build Status](https://travis-ci.org/dhowden/tag.svg?branch=master)](https://travis-ci.org/dhowden/tag)
+[![GoDoc](https://godoc.org/github.com/dhowden/tag?status.svg)](https://godoc.org/github.com/dhowden/tag)
+
+This package provides MP3 (ID3v1,2.{2,3,4}) and MP4 (ACC, M4A, ALAC), OGG and FLAC metadata detection, parsing and artwork extraction.
+
+Detect and parse tag metadata from an `io.ReadSeeker` (i.e. an `*os.File`):
+
+```go
+m, err := tag.ReadFrom(f)
+if err != nil {
+ log.Fatal(err)
+}
+log.Print(m.Format()) // The detected format.
+log.Print(m.Title()) // The title of the track (see Metadata interface for more details).
+```
+
+Parsed metadata is exported via a single interface (giving a consistent API for all supported metadata formats).
+
+```go
+// Metadata is an interface which is used to describe metadata retrieved by this package.
+type Metadata interface {
+ Format() Format
+ FileType() FileType
+
+ Title() string
+ Album() string
+ Artist() string
+ AlbumArtist() string
+ Composer() string
+ Genre() string
+ Year() int
+
+ Track() (int, int) // Number, Total
+ Disc() (int, int) // Number, Total
+
+ Picture() *Picture // Artwork
+ Lyrics() string
+ Comment() string
+
+ Raw() map[string]interface{} // NB: raw tag names are not consistent across formats.
+}
+```
+
+## Audio Data Checksum (SHA1)
+
+This package also provides a metadata-invariant checksum for audio files: only the audio data is used to
+construct the checksum.
+
+[http://godoc.org/github.com/dhowden/tag#Sum](http://godoc.org/github.com/dhowden/tag#Sum)
+
+## Tools
+
+There are simple command-line tools which demonstrate basic tag extraction and summing:
+
+```console
+$ go get github.com/dhowden/tag/...
+$ cd $GOPATH/bin
+$ ./tag 11\ High\ Hopes.m4a
+Metadata Format: MP4
+Title: High Hopes
+Album: The Division Bell
+Artist: Pink Floyd
+Composer: Abbey Road Recording Studios/David Gilmour/Polly Samson
+Year: 1994
+Track: 11 of 11
+Disc: 1 of 1
+Picture: Picture{Ext: jpeg, MIMEType: image/jpeg, Type: , Description: , Data.Size: 606109}
+
+$ ./sum 11\ High\ Hopes.m4a
+2ae208c5f00a1f21f5fac9b7f6e0b8e52c06da29
+```
diff --git a/vendor/github.com/dhowden/tag/dsf.go b/vendor/github.com/dhowden/tag/dsf.go
new file mode 100644
index 0000000..d826a74
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/dsf.go
@@ -0,0 +1,110 @@
+// 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"
+ "io"
+)
+
+// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+// samples: http://www.2l.no/hires/index.html
+func ReadDSFTags(r io.ReadSeeker) (Metadata, error) {
+ dsd, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+ if dsd != "DSD " {
+ return nil, errors.New("expected 'DSD '")
+ }
+
+ _, err = r.Seek(int64(16), io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ n4, err := readBytes(r, 8)
+ if err != nil {
+ return nil, err
+ }
+ id3Pointer := getIntLittleEndian(n4)
+
+ _, err = r.Seek(int64(id3Pointer), io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ id3, err := ReadID3v2Tags(r)
+ if err != nil {
+ return nil, err
+ }
+
+ return metadataDSF{id3}, nil
+}
+
+type metadataDSF struct {
+ id3 Metadata
+}
+
+func (m metadataDSF) Format() Format {
+ return m.id3.Format()
+}
+
+func (m metadataDSF) FileType() FileType {
+ return DSF
+}
+
+func (m metadataDSF) Title() string {
+ return m.id3.Title()
+}
+
+func (m metadataDSF) Album() string {
+ return m.id3.Album()
+}
+
+func (m metadataDSF) Artist() string {
+ return m.id3.Artist()
+}
+
+func (m metadataDSF) AlbumArtist() string {
+ return m.id3.AlbumArtist()
+}
+
+func (m metadataDSF) Composer() string {
+ return m.id3.Composer()
+}
+
+func (m metadataDSF) Year() int {
+ return m.id3.Year()
+}
+
+func (m metadataDSF) Genre() string {
+ return m.id3.Genre()
+}
+
+func (m metadataDSF) Track() (int, int) {
+ return m.id3.Track()
+}
+
+func (m metadataDSF) Disc() (int, int) {
+ return m.id3.Disc()
+}
+
+func (m metadataDSF) Picture() *Picture {
+ return m.id3.Picture()
+}
+
+func (m metadataDSF) Lyrics() string {
+ return m.id3.Lyrics()
+}
+
+func (m metadataDSF) Comment() string {
+ return m.id3.Comment()
+}
+
+func (m metadataDSF) Raw() map[string]interface{} {
+ return m.id3.Raw()
+}
diff --git a/vendor/github.com/dhowden/tag/flac.go b/vendor/github.com/dhowden/tag/flac.go
new file mode 100644
index 0000000..c370467
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/flac.go
@@ -0,0 +1,89 @@
+// 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"
+ "io"
+)
+
+// blockType is a type which represents an enumeration of valid FLAC blocks
+type blockType byte
+
+// FLAC block types.
+const (
+ // Stream Info Block 0
+ // Padding Block 1
+ // Application Block 2
+ // Seektable Block 3
+ // Cue Sheet Block 5
+ vorbisCommentBlock blockType = 4
+ pictureBlock blockType = 6
+)
+
+// 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) {
+ flac, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+ if flac != "fLaC" {
+ return nil, errors.New("expected 'fLaC'")
+ }
+
+ m := &metadataFLAC{
+ newMetadataVorbis(),
+ }
+
+ for {
+ last, err := m.readFLACMetadataBlock(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if last {
+ break
+ }
+ }
+ return m, nil
+}
+
+type metadataFLAC struct {
+ *metadataVorbis
+}
+
+func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+ blockHeader, err := readBytes(r, 1)
+ if err != nil {
+ return
+ }
+
+ if getBit(blockHeader[0], 7) {
+ blockHeader[0] ^= (1 << 7)
+ last = true
+ }
+
+ blockLen, err := readInt(r, 3)
+ if err != nil {
+ return
+ }
+
+ switch blockType(blockHeader[0]) {
+ case vorbisCommentBlock:
+ err = m.readVorbisComment(r)
+
+ case pictureBlock:
+ err = m.readPictureBlock(r)
+
+ default:
+ _, err = r.Seek(int64(blockLen), io.SeekCurrent)
+ }
+ return
+}
+
+func (m *metadataFLAC) FileType() FileType {
+ return FLAC
+}
diff --git a/vendor/github.com/dhowden/tag/id.go b/vendor/github.com/dhowden/tag/id.go
new file mode 100644
index 0000000..2410356
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id.go
@@ -0,0 +1,81 @@
+package tag
+
+import (
+ "fmt"
+ "io"
+)
+
+// Identify identifies the format and file type of the data in the ReadSeeker.
+func Identify(r io.ReadSeeker) (format Format, fileType FileType, err error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ err = fmt.Errorf("could not seek back to original position: %v", err)
+ return
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return VORBIS, FLAC, nil
+
+ case string(b[0:4]) == "OggS":
+ return VORBIS, OGG, nil
+
+ case string(b[4:8]) == "ftyp":
+ b = b[8:11]
+ fileType = UnknownFileType
+ switch string(b) {
+ case "M4A":
+ fileType = M4A
+
+ case "M4B":
+ fileType = M4B
+
+ case "M4P":
+ fileType = M4P
+ }
+ return MP4, fileType, nil
+
+ case string(b[0:3]) == "ID3":
+ b := b[3:]
+ switch uint(b[0]) {
+ case 2:
+ format = ID3v2_2
+ case 3:
+ format = ID3v2_3
+ case 4:
+ format = ID3v2_4
+ case 0, 1:
+ fallthrough
+ default:
+ err = fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
+ return
+ }
+ return format, MP3, nil
+ }
+
+ n, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return
+ }
+
+ tag, err := readString(r, 3)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(-n, io.SeekCurrent)
+ if err != nil {
+ return
+ }
+
+ if tag != "TAG" {
+ err = ErrNoTagsFound
+ return
+ }
+ return ID3v1, MP3, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v1.go b/vendor/github.com/dhowden/tag/id3v1.go
new file mode 100644
index 0000000..0953f0b
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v1.go
@@ -0,0 +1,144 @@
+// 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"
+ "io"
+ "strconv"
+ "strings"
+)
+
+// id3v1Genres is a list of genres as given in the ID3v1 specification.
+var id3v1Genres = [...]string{
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+ "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+ "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+ "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
+ "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
+ "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
+ "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
+ "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+ "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
+ "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
+ "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
+ "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
+ "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
+ "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
+ "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
+ "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
+ "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
+ "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+ "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
+ "Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall",
+}
+
+// ErrNotID3v1 is an error which is returned when no ID3v1 header is found.
+var ErrNotID3v1 = errors.New("invalid ID3v1 header")
+
+// ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker. Returns ErrNotID3v1
+// if there are no ID3v1 tags, otherwise non-nil error if there was a problem.
+func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) {
+ _, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return nil, err
+ }
+
+ if tag, err := readString(r, 3); err != nil {
+ return nil, err
+ } else if tag != "TAG" {
+ return nil, ErrNotID3v1
+ }
+
+ title, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ artist, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ album, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ year, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+
+ commentBytes, err := readBytes(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ var comment string
+ var track int
+ if commentBytes[28] == 0 {
+ comment = trimString(string(commentBytes[:28]))
+ track = int(commentBytes[29])
+ } else {
+ comment = trimString(string(commentBytes))
+ }
+
+ var genre string
+ genreID, err := readBytes(r, 1)
+ if err != nil {
+ return nil, err
+ }
+ if int(genreID[0]) < len(id3v1Genres) {
+ genre = id3v1Genres[int(genreID[0])]
+ }
+
+ m := make(map[string]interface{})
+ m["title"] = trimString(title)
+ m["artist"] = trimString(artist)
+ m["album"] = trimString(album)
+ m["year"] = trimString(year)
+ m["comment"] = trimString(comment)
+ m["track"] = track
+ m["genre"] = genre
+
+ return metadataID3v1(m), nil
+}
+
+func trimString(x string) string {
+ return strings.TrimSpace(strings.Trim(x, "\x00"))
+}
+
+// metadataID3v1 is the implementation of Metadata used for ID3v1 tags.
+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) }
+func (m metadataID3v1) Album() string { return m["album"].(string) }
+func (m metadataID3v1) Artist() string { return m["artist"].(string) }
+func (m metadataID3v1) Genre() string { return m["genre"].(string) }
+
+func (m metadataID3v1) Year() int {
+ y := m["year"].(string)
+ n, err := strconv.Atoi(y)
+ if err != nil {
+ return 0
+ }
+ return n
+}
+
+func (m metadataID3v1) Track() (int, int) { return m["track"].(int), 0 }
+
+func (m metadataID3v1) AlbumArtist() string { return "" }
+func (m metadataID3v1) Composer() string { return "" }
+func (metadataID3v1) Disc() (int, int) { return 0, 0 }
+func (m metadataID3v1) Picture() *Picture { return nil }
+func (m metadataID3v1) Lyrics() string { return "" }
+func (m metadataID3v1) Comment() string { return m["comment"].(string) }
diff --git a/vendor/github.com/dhowden/tag/id3v2.go b/vendor/github.com/dhowden/tag/id3v2.go
new file mode 100644
index 0000000..063e6cb
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2.go
@@ -0,0 +1,434 @@
+// 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 (
+ "fmt"
+ "io"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+var id3v2Genres = [...]string{
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+ "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+ "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+ "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
+ "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
+ "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
+ "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
+ "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+ "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
+ "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
+ "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
+ "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer",
+ "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
+ "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
+ "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
+ "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
+ "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
+ "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+ "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
+ "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall",
+ "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie",
+ "Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap",
+ "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian",
+ "Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop",
+ "Synthpop",
+}
+
+// id3v2Header is a type which represents an ID3v2 tag header.
+type id3v2Header struct {
+ Version Format
+ Unsynchronisation bool
+ ExtendedHeader bool
+ Experimental bool
+ Size int
+}
+
+// readID3v2Header reads the ID3v2 header from the given io.Reader.
+// offset it number of bytes of header that was read
+func readID3v2Header(r io.Reader) (h *id3v2Header, offset int, err error) {
+ offset = 10
+ b, err := readBytes(r, offset)
+ if err != nil {
+ return nil, 0, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err)
+ }
+
+ if string(b[0:3]) != "ID3" {
+ return nil, 0, fmt.Errorf("expected to read \"ID3\"")
+ }
+
+ b = b[3:]
+ var vers Format
+ switch uint(b[0]) {
+ case 2:
+ vers = ID3v2_2
+ case 3:
+ vers = ID3v2_3
+ case 4:
+ vers = ID3v2_4
+ case 0, 1:
+ fallthrough
+ default:
+ return nil, 0, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
+ }
+
+ // NB: We ignore b[1] (the revision) as we don't currently rely on it.
+ h = &id3v2Header{
+ Version: vers,
+ Unsynchronisation: getBit(b[2], 7),
+ ExtendedHeader: getBit(b[2], 6),
+ Experimental: getBit(b[2], 5),
+ Size: get7BitChunkedInt(b[3:7]),
+ }
+
+ if h.ExtendedHeader {
+ switch vers {
+ case ID3v2_3:
+ b, err := readBytes(r, 4)
+ if err != nil {
+ return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v23 extended header len): %v", err)
+ }
+ // skip header, size is excluding len bytes
+ extendedHeaderSize := getInt(b)
+ _, err = readBytes(r, extendedHeaderSize)
+ if err != nil {
+ return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v23 skip extended header): %v", extendedHeaderSize, err)
+ }
+ offset += extendedHeaderSize
+ case ID3v2_4:
+ b, err := readBytes(r, 4)
+ if err != nil {
+ return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v24 extended header len): %v", err)
+ }
+ // skip header, size is synchsafe int including len bytes
+ extendedHeaderSize := get7BitChunkedInt(b) - 4
+ _, err = readBytes(r, extendedHeaderSize)
+ if err != nil {
+ return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v24 skip extended header): %v", extendedHeaderSize, err)
+ }
+ offset += extendedHeaderSize
+ default:
+ // nop, only 2.3 and 2.4 should have extended header
+ }
+ }
+
+ return h, offset, nil
+}
+
+// id3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame.
+type id3v2FrameFlags struct {
+ // Message (ID3 2.3.0 and 2.4.0)
+ TagAlterPreservation bool
+ FileAlterPreservation bool
+ ReadOnly bool
+
+ // Format (ID3 2.3.0 and 2.4.0)
+ Compression bool
+ Encryption bool
+ GroupIdentity bool
+ // ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1)
+ Unsynchronisation bool
+ DataLengthIndicator bool
+}
+
+func readID3v23FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
+ b, err := readBytes(r, 2)
+ if err != nil {
+ return nil, err
+ }
+
+ msg := b[0]
+ fmt := b[1]
+
+ return &id3v2FrameFlags{
+ TagAlterPreservation: getBit(msg, 7),
+ FileAlterPreservation: getBit(msg, 6),
+ ReadOnly: getBit(msg, 5),
+ Compression: getBit(fmt, 7),
+ Encryption: getBit(fmt, 6),
+ GroupIdentity: getBit(fmt, 5),
+ }, nil
+}
+
+func readID3v24FrameFlags(r io.Reader) (*id3v2FrameFlags, error) {
+ b, err := readBytes(r, 2)
+ if err != nil {
+ return nil, err
+ }
+
+ msg := b[0]
+ fmt := b[1]
+
+ return &id3v2FrameFlags{
+ TagAlterPreservation: getBit(msg, 6),
+ FileAlterPreservation: getBit(msg, 5),
+ ReadOnly: getBit(msg, 4),
+ GroupIdentity: getBit(fmt, 6),
+ Compression: getBit(fmt, 3),
+ Encryption: getBit(fmt, 2),
+ Unsynchronisation: getBit(fmt, 1),
+ DataLengthIndicator: getBit(fmt, 0),
+ }, nil
+
+}
+
+func readID3v2_2FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
+ name, err = readString(r, 3)
+ if err != nil {
+ return
+ }
+ size, err = readInt(r, 3)
+ if err != nil {
+ return
+ }
+ headerSize = 6
+ return
+}
+
+func readID3v2_3FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
+ name, err = readString(r, 4)
+ if err != nil {
+ return
+ }
+ size, err = readInt(r, 4)
+ if err != nil {
+ return
+ }
+ headerSize = 8
+ return
+}
+
+func readID3v2_4FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) {
+ name, err = readString(r, 4)
+ if err != nil {
+ return
+ }
+ size, err = read7BitChunkedInt(r, 4)
+ if err != nil {
+ return
+ }
+ headerSize = 8
+ return
+}
+
+// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header.
+func readID3v2Frames(r io.Reader, offset int, h *id3v2Header) (map[string]interface{}, error) {
+ result := make(map[string]interface{})
+
+ for offset < h.Size {
+ var err error
+ var name string
+ var size, headerSize int
+ var flags *id3v2FrameFlags
+
+ switch h.Version {
+ case ID3v2_2:
+ name, size, headerSize, err = readID3v2_2FrameHeader(r)
+
+ case ID3v2_3:
+ name, size, headerSize, err = readID3v2_3FrameHeader(r)
+ if err != nil {
+ return nil, err
+ }
+ flags, err = readID3v23FrameFlags(r)
+ headerSize += 2
+
+ case ID3v2_4:
+ name, size, headerSize, err = readID3v2_4FrameHeader(r)
+ if err != nil {
+ return nil, err
+ }
+ flags, err = readID3v24FrameFlags(r)
+ headerSize += 2
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ // FIXME: Do we still need this?
+ // if size=0, we certainly are in a padding zone. ignore the rest of
+ // the tags
+ if size == 0 {
+ break
+ }
+
+ offset += headerSize + size
+
+ // Avoid corrupted padding (see http://id3.org/Compliance%20Issues).
+ if !validID3Frame(h.Version, name) && offset > h.Size {
+ break
+ }
+
+ if flags != nil {
+ if flags.Compression {
+ _, err = read7BitChunkedInt(r, 4) // read 4
+ if err != nil {
+ return nil, err
+ }
+ size -= 4
+ }
+
+ if flags.Encryption {
+ _, err = readBytes(r, 1) // read 1 byte of encryption method
+ if err != nil {
+ return nil, err
+ }
+ size -= 1
+ }
+ }
+
+ b, err := readBytes(r, size)
+ if err != nil {
+ return nil, err
+ }
+
+ // There can be multiple tag with the same name. Append a number to the
+ // name if there is more than one.
+ rawName := name
+ if _, ok := result[rawName]; ok {
+ for i := 0; ok; i++ {
+ rawName = name + "_" + strconv.Itoa(i)
+ _, ok = result[rawName]
+ }
+ }
+
+ switch {
+ case name == "TXXX" || name == "TXX":
+ t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = t
+
+ case name[0] == 'T':
+ txt, err := readTFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = txt
+
+ case name == "UFID" || name == "UFI":
+ t, err := readUFID(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = t
+
+ case name == "WXXX" || name == "WXX":
+ t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = t
+
+ case name[0] == 'W':
+ txt, err := readWFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = txt
+
+ case name == "COMM" || name == "COM" || name == "USLT" || name == "ULT":
+ t, err := readTextWithDescrFrame(b, true, true) // both lang and enc
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = t
+
+ case name == "APIC":
+ p, err := readAPICFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = p
+
+ case name == "PIC":
+ p, err := readPICFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = p
+
+ default:
+ result[rawName] = b
+ }
+ }
+ return result, nil
+}
+
+type unsynchroniser struct {
+ io.Reader
+ ff bool
+}
+
+// filter io.Reader which skip the Unsynchronisation bytes
+func (r *unsynchroniser) Read(p []byte) (int, error) {
+ b := make([]byte, 1)
+ i := 0
+ for i < len(p) {
+ if n, err := r.Reader.Read(b); err != nil || n == 0 {
+ return i, err
+ }
+ if r.ff && b[0] == 0x00 {
+ r.ff = false
+ continue
+ }
+ p[i] = b[0]
+ i++
+ r.ff = (b[0] == 0xFF)
+ }
+ return i, nil
+}
+
+// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
+// non-nil error on failure.
+func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
+ h, offset, err := readID3v2Header(r)
+ if err != nil {
+ return nil, err
+ }
+
+ var ur io.Reader = r
+ if h.Unsynchronisation {
+ ur = &unsynchroniser{Reader: r}
+ }
+
+ f, err := readID3v2Frames(ur, offset, h)
+ if err != nil {
+ return nil, err
+ }
+ return metadataID3v2{header: h, frames: f}, nil
+}
+
+var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`)
+
+// id3v2genre parse a id3v2 genre tag and expand the numeric genres
+func id3v2genre(genre string) string {
+ c := true
+ for c {
+ orig := genre
+ if match := id3v2genreRe.FindStringSubmatch(genre); len(match) > 0 {
+ if genreID, err := strconv.Atoi(match[2]); err == nil {
+ if genreID < len(id3v2Genres) {
+ genre = id3v2Genres[genreID]
+ if match[1] != "" {
+ genre = strings.TrimSpace(match[1]) + " " + genre
+ }
+ if match[3] != "" {
+ genre = genre + " " + match[3]
+ }
+ }
+ }
+ }
+ c = (orig != genre)
+ }
+ return strings.Replace(genre, "((", "(", -1)
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2frames.go b/vendor/github.com/dhowden/tag/id3v2frames.go
new file mode 100644
index 0000000..a92afa1
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2frames.go
@@ -0,0 +1,638 @@
+// 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"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "strings"
+ "unicode/utf16"
+)
+
+// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding
+// is specified without a corresponding BOM in the data.
+var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian
+
+// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4).
+var id3v22Frames = map[string]string{
+ "BUF": "Recommended buffer size",
+
+ "CNT": "Play counter",
+ "COM": "Comments",
+ "CRA": "Audio encryption",
+ "CRM": "Encrypted meta frame",
+
+ "ETC": "Event timing codes",
+ "EQU": "Equalization",
+
+ "GEO": "General encapsulated object",
+
+ "IPL": "Involved people list",
+
+ "LNK": "Linked information",
+
+ "MCI": "Music CD Identifier",
+ "MLL": "MPEG location lookup table",
+
+ "PIC": "Attached picture",
+ "POP": "Popularimeter",
+
+ "REV": "Reverb",
+ "RVA": "Relative volume adjustment",
+
+ "SLT": "Synchronized lyric/text",
+ "STC": "Synced tempo codes",
+
+ "TAL": "Album/Movie/Show title",
+ "TBP": "BPM (Beats Per Minute)",
+ "TCM": "Composer",
+ "TCO": "Content type",
+ "TCR": "Copyright message",
+ "TDA": "Date",
+ "TDY": "Playlist delay",
+ "TEN": "Encoded by",
+ "TFT": "File type",
+ "TIM": "Time",
+ "TKE": "Initial key",
+ "TLA": "Language(s)",
+ "TLE": "Length",
+ "TMT": "Media type",
+ "TOA": "Original artist(s)/performer(s)",
+ "TOF": "Original filename",
+ "TOL": "Original Lyricist(s)/text writer(s)",
+ "TOR": "Original release year",
+ "TOT": "Original album/Movie/Show title",
+ "TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
+ "TP2": "Band/Orchestra/Accompaniment",
+ "TP3": "Conductor/Performer refinement",
+ "TP4": "Interpreted, remixed, or otherwise modified by",
+ "TPA": "Part of a set",
+ "TPB": "Publisher",
+ "TRC": "ISRC (International Standard Recording Code)",
+ "TRD": "Recording dates",
+ "TRK": "Track number/Position in set",
+ "TSI": "Size",
+ "TSS": "Software/hardware and settings used for encoding",
+ "TT1": "Content group description",
+ "TT2": "Title/Songname/Content description",
+ "TT3": "Subtitle/Description refinement",
+ "TXT": "Lyricist/text writer",
+ "TXX": "User defined text information frame",
+ "TYE": "Year",
+
+ "UFI": "Unique file identifier",
+ "ULT": "Unsychronized lyric/text transcription",
+
+ "WAF": "Official audio file webpage",
+ "WAR": "Official artist/performer webpage",
+ "WAS": "Official audio source webpage",
+ "WCM": "Commercial information",
+ "WCP": "Copyright/Legal information",
+ "WPB": "Publishers official webpage",
+ "WXX": "User defined URL link frame",
+}
+
+// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
+var id3v23Frames = map[string]string{
+ "AENC": "Audio encryption]",
+ "APIC": "Attached picture",
+ "COMM": "Comments",
+ "COMR": "Commercial frame",
+ "ENCR": "Encryption method registration",
+ "EQUA": "Equalization",
+ "ETCO": "Event timing codes",
+ "GEOB": "General encapsulated object",
+ "GRID": "Group identification registration",
+ "IPLS": "Involved people list",
+ "LINK": "Linked information",
+ "MCDI": "Music CD identifier",
+ "MLLT": "MPEG location lookup table",
+ "OWNE": "Ownership frame",
+ "PRIV": "Private frame",
+ "PCNT": "Play counter",
+ "POPM": "Popularimeter",
+ "POSS": "Position synchronisation frame",
+ "RBUF": "Recommended buffer size",
+ "RVAD": "Relative volume adjustment",
+ "RVRB": "Reverb",
+ "SYLT": "Synchronized lyric/text",
+ "SYTC": "Synchronized tempo codes",
+ "TALB": "Album/Movie/Show title",
+ "TBPM": "BPM (beats per minute)",
+ "TCMP": "iTunes Compilation Flag",
+ "TCOM": "Composer",
+ "TCON": "Content type",
+ "TCOP": "Copyright message",
+ "TDAT": "Date",
+ "TDLY": "Playlist delay",
+ "TENC": "Encoded by",
+ "TEXT": "Lyricist/Text writer",
+ "TFLT": "File type",
+ "TIME": "Time",
+ "TIT1": "Content group description",
+ "TIT2": "Title/songname/content description",
+ "TIT3": "Subtitle/Description refinement",
+ "TKEY": "Initial key",
+ "TLAN": "Language(s)",
+ "TLEN": "Length",
+ "TMED": "Media type",
+ "TOAL": "Original album/movie/show title",
+ "TOFN": "Original filename",
+ "TOLY": "Original lyricist(s)/text writer(s)",
+ "TOPE": "Original artist(s)/performer(s)",
+ "TORY": "Original release year",
+ "TOWN": "File owner/licensee",
+ "TPE1": "Lead performer(s)/Soloist(s)",
+ "TPE2": "Band/orchestra/accompaniment",
+ "TPE3": "Conductor/performer refinement",
+ "TPE4": "Interpreted, remixed, or otherwise modified by",
+ "TPOS": "Part of a set",
+ "TPUB": "Publisher",
+ "TRCK": "Track number/Position in set",
+ "TRDA": "Recording dates",
+ "TRSN": "Internet radio station name",
+ "TRSO": "Internet radio station owner",
+ "TSIZ": "Size",
+ "TSO2": "iTunes uses this for Album Artist sort order",
+ "TSOC": "iTunes uses this for Composer sort order",
+ "TSRC": "ISRC (international standard recording code)",
+ "TSSE": "Software/Hardware and settings used for encoding",
+ "TYER": "Year",
+ "TXXX": "User defined text information frame",
+ "UFID": "Unique file identifier",
+ "USER": "Terms of use",
+ "USLT": "Unsychronized lyric/text transcription",
+ "WCOM": "Commercial information",
+ "WCOP": "Copyright/Legal information",
+ "WOAF": "Official audio file webpage",
+ "WOAR": "Official artist/performer webpage",
+ "WOAS": "Official audio source webpage",
+ "WORS": "Official internet radio station homepage",
+ "WPAY": "Payment",
+ "WPUB": "Publishers official webpage",
+ "WXXX": "User defined URL link frame",
+}
+
+// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4).
+var id3v24Frames = map[string]string{
+ "AENC": "Audio encryption",
+ "APIC": "Attached picture",
+ "ASPI": "Audio seek point index",
+
+ "COMM": "Comments",
+ "COMR": "Commercial frame",
+
+ "ENCR": "Encryption method registration",
+ "EQU2": "Equalisation (2)",
+ "ETCO": "Event timing codes",
+
+ "GEOB": "General encapsulated object",
+ "GRID": "Group identification registration",
+
+ "LINK": "Linked information",
+
+ "MCDI": "Music CD identifier",
+ "MLLT": "MPEG location lookup table",
+
+ "OWNE": "Ownership frame",
+
+ "PRIV": "Private frame",
+ "PCNT": "Play counter",
+ "POPM": "Popularimeter",
+ "POSS": "Position synchronisation frame",
+
+ "RBUF": "Recommended buffer size",
+ "RVA2": "Relative volume adjustment (2)",
+ "RVRB": "Reverb",
+
+ "SEEK": "Seek frame",
+ "SIGN": "Signature frame",
+ "SYLT": "Synchronised lyric/text",
+ "SYTC": "Synchronised tempo codes",
+
+ "TALB": "Album/Movie/Show title",
+ "TBPM": "BPM (beats per minute)",
+ "TCMP": "iTunes Compilation Flag",
+ "TCOM": "Composer",
+ "TCON": "Content type",
+ "TCOP": "Copyright message",
+ "TDEN": "Encoding time",
+ "TDLY": "Playlist delay",
+ "TDOR": "Original release time",
+ "TDRC": "Recording time",
+ "TDRL": "Release time",
+ "TDTG": "Tagging time",
+ "TENC": "Encoded by",
+ "TEXT": "Lyricist/Text writer",
+ "TFLT": "File type",
+ "TIPL": "Involved people list",
+ "TIT1": "Content group description",
+ "TIT2": "Title/songname/content description",
+ "TIT3": "Subtitle/Description refinement",
+ "TKEY": "Initial key",
+ "TLAN": "Language(s)",
+ "TLEN": "Length",
+ "TMCL": "Musician credits list",
+ "TMED": "Media type",
+ "TMOO": "Mood",
+ "TOAL": "Original album/movie/show title",
+ "TOFN": "Original filename",
+ "TOLY": "Original lyricist(s)/text writer(s)",
+ "TOPE": "Original artist(s)/performer(s)",
+ "TOWN": "File owner/licensee",
+ "TPE1": "Lead performer(s)/Soloist(s)",
+ "TPE2": "Band/orchestra/accompaniment",
+ "TPE3": "Conductor/performer refinement",
+ "TPE4": "Interpreted, remixed, or otherwise modified by",
+ "TPOS": "Part of a set",
+ "TPRO": "Produced notice",
+ "TPUB": "Publisher",
+ "TRCK": "Track number/Position in set",
+ "TRSN": "Internet radio station name",
+ "TRSO": "Internet radio station owner",
+ "TSO2": "iTunes uses this for Album Artist sort order",
+ "TSOA": "Album sort order",
+ "TSOC": "iTunes uses this for Composer sort order",
+ "TSOP": "Performer sort order",
+ "TSOT": "Title sort order",
+ "TSRC": "ISRC (international standard recording code)",
+ "TSSE": "Software/Hardware and settings used for encoding",
+ "TSST": "Set subtitle",
+ "TXXX": "User defined text information frame",
+
+ "UFID": "Unique file identifier",
+ "USER": "Terms of use",
+ "USLT": "Unsynchronised lyric/text transcription",
+
+ "WCOM": "Commercial information",
+ "WCOP": "Copyright/Legal information",
+ "WOAF": "Official audio file webpage",
+ "WOAR": "Official artist/performer webpage",
+ "WOAS": "Official audio source webpage",
+ "WORS": "Official Internet radio station homepage",
+ "WPAY": "Payment",
+ "WPUB": "Publishers official webpage",
+ "WXXX": "User defined URL link frame",
+}
+
+// ID3 frames that are defined in the specs.
+var id3Frames = map[Format]map[string]string{
+ ID3v2_2: id3v22Frames,
+ ID3v2_3: id3v23Frames,
+ ID3v2_4: id3v24Frames,
+}
+
+func validID3Frame(version Format, name string) bool {
+ names, ok := id3Frames[version]
+ if !ok {
+ return false
+ }
+ _, ok = names[name]
+ return ok
+}
+
+func readWFrame(b []byte) (string, error) {
+ // Frame text is always encoded in ISO-8859-1
+ b = append([]byte{0}, b...)
+ return readTFrame(b)
+}
+
+func readTFrame(b []byte) (string, error) {
+ if len(b) == 0 {
+ return "", nil
+ }
+
+ txt, err := decodeText(b[0], b[1:])
+ if err != nil {
+ return "", err
+ }
+ return strings.Join(strings.Split(txt, string(singleZero)), ""), nil
+}
+
+const (
+ encodingISO8859 byte = 0
+ encodingUTF16WithBOM byte = 1
+ encodingUTF16 byte = 2
+ encodingUTF8 byte = 3
+)
+
+func decodeText(enc byte, b []byte) (string, error) {
+ if len(b) == 0 {
+ return "", nil
+ }
+
+ switch enc {
+ case encodingISO8859: // ISO-8859-1
+ return decodeISO8859(b), nil
+
+ case encodingUTF16WithBOM: // UTF-16 with byte order marker
+ if len(b) == 1 {
+ return "", nil
+ }
+ return decodeUTF16WithBOM(b)
+
+ case encodingUTF16: // UTF-16 without byte order (assuming BigEndian)
+ if len(b) == 1 {
+ return "", nil
+ }
+ return decodeUTF16(b, binary.BigEndian)
+
+ case encodingUTF8: // UTF-8
+ return string(b), nil
+
+ default: // Fallback to ISO-8859-1
+ return decodeISO8859(b), nil
+ }
+}
+
+var (
+ singleZero = []byte{0}
+ doubleZero = []byte{0, 0}
+)
+
+func dataSplit(b []byte, enc byte) [][]byte {
+ delim := singleZero
+ if enc == encodingUTF16 || enc == encodingUTF16WithBOM {
+ delim = doubleZero
+ }
+
+ result := bytes.SplitN(b, delim, 2)
+ if len(result) != 2 {
+ return result
+ }
+
+ if len(result[1]) == 0 {
+ return result
+ }
+
+ if result[1][0] == 0 {
+ // there was a double (or triple) 0 and we cut too early
+ result[0] = append(result[0], result[1][0])
+ result[1] = result[1][1:]
+ }
+ return result
+}
+
+func decodeISO8859(b []byte) string {
+ r := make([]rune, len(b))
+ for i, x := range b {
+ r[i] = rune(x)
+ }
+ return string(r)
+}
+
+func decodeUTF16WithBOM(b []byte) (string, error) {
+ if len(b) < 2 {
+ return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark")
+ }
+
+ var bo binary.ByteOrder
+ switch {
+ case b[0] == 0xFE && b[1] == 0xFF:
+ bo = binary.BigEndian
+ b = b[2:]
+
+ case b[0] == 0xFF && b[1] == 0xFE:
+ bo = binary.LittleEndian
+ b = b[2:]
+
+ default:
+ bo = DefaultUTF16WithBOMByteOrder
+ }
+ return decodeUTF16(b, bo)
+}
+
+func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) {
+ if len(b)%2 != 0 {
+ return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text")
+ }
+ s := make([]uint16, 0, len(b)/2)
+ for i := 0; i < len(b); i += 2 {
+ s = append(s, bo.Uint16(b[i:i+2]))
+ }
+ return string(utf16.Decode(s)), nil
+}
+
+// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag.
+// It's a text with a description and a specified language
+// For WXXX, TXXX and UFID, we don't set a Language
+type Comm struct {
+ Language string
+ Description string
+ Text string
+}
+
+// String returns a string representation of the underlying Comm instance.
+func (t Comm) String() string {
+ if t.Language != "" {
+ return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}",
+ t.Language, t.Description, strings.Count(t.Text, "\n"))
+ }
+ return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text)
+}
+
+// IDv2.{3,4}
+// -- Header
+// <Header for 'Unsynchronised lyrics/text transcription', ID: "USLT">
+// <Header for 'Comment', ID: "COMM">
+// -- readTextWithDescrFrame(data, true, true)
+// Text encoding $xx
+// Language $xx xx xx
+// Content descriptor <text string according to encoding> $00 (00)
+// Lyrics/text <full text string according to encoding>
+// -- Header
+// <Header for 'User defined text information frame', ID: "TXXX">
+// <Header for 'User defined URL link frame', ID: "WXXX">
+// -- readTextWithDescrFrame(data, false, <isDataEncoded>)
+// Text encoding $xx
+// Description <text string according to encoding> $00 (00)
+// Value <text string according to encoding>
+func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) {
+ enc := b[0]
+ b = b[1:]
+
+ c := &Comm{}
+ if hasLang {
+ c.Language = string(b[:3])
+ b = b[3:]
+ }
+
+ descTextSplit := dataSplit(b, enc)
+ if len(descTextSplit) < 1 {
+ return nil, fmt.Errorf("error decoding tag description text: invalid encoding")
+ }
+
+ desc, err := decodeText(enc, descTextSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding tag description text: %v", err)
+ }
+ c.Description = desc
+
+ if len(descTextSplit) == 1 {
+ return c, nil
+ }
+
+ if !encoded {
+ enc = byte(0)
+ }
+ text, err := decodeText(enc, descTextSplit[1])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding tag text: %v", err)
+ }
+ c.Text = text
+
+ return c, nil
+}
+
+// UFID is composed of a provider (frequently a URL and a binary identifier)
+// The identifier can be a text (Musicbrainz use texts, but not necessary)
+type UFID struct {
+ Provider string
+ Identifier []byte
+}
+
+func (u UFID) String() string {
+ return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier))
+}
+
+func readUFID(b []byte) (*UFID, error) {
+ result := bytes.SplitN(b, singleZero, 2)
+ if len(result) != 2 {
+ return nil, errors.New("expected to split UFID data into 2 pieces")
+ }
+
+ return &UFID{
+ Provider: string(result[0]),
+ Identifier: result[1],
+ }, nil
+}
+
+var pictureTypes = map[byte]string{
+ 0x00: "Other",
+ 0x01: "32x32 pixels 'file icon' (PNG only)",
+ 0x02: "Other file icon",
+ 0x03: "Cover (front)",
+ 0x04: "Cover (back)",
+ 0x05: "Leaflet page",
+ 0x06: "Media (e.g. lable side of CD)",
+ 0x07: "Lead artist/lead performer/soloist",
+ 0x08: "Artist/performer",
+ 0x09: "Conductor",
+ 0x0A: "Band/Orchestra",
+ 0x0B: "Composer",
+ 0x0C: "Lyricist/text writer",
+ 0x0D: "Recording Location",
+ 0x0E: "During recording",
+ 0x0F: "During performance",
+ 0x10: "Movie/video screen capture",
+ 0x11: "A bright coloured fish",
+ 0x12: "Illustration",
+ 0x13: "Band/artist logotype",
+ 0x14: "Publisher/Studio logotype",
+}
+
+// Picture is a type which represents an attached picture extracted from metadata.
+type Picture struct {
+ Ext string // Extension of the picture file.
+ MIMEType string // MIMEType of the picture.
+ Type string // Type of the picture (see pictureTypes).
+ Description string // Description.
+ Data []byte // Raw picture data.
+}
+
+// String returns a string representation of the underlying Picture instance.
+func (p Picture) String() string {
+ return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}",
+ p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data))
+}
+
+// IDv2.2
+// -- Header
+// Attached picture "PIC"
+// Frame size $xx xx xx
+// -- readPICFrame
+// Text encoding $xx
+// Image format $xx xx xx
+// Picture type $xx
+// Description <textstring> $00 (00)
+// Picture data <binary data>
+func readPICFrame(b []byte) (*Picture, error) {
+ enc := b[0]
+ ext := string(b[1:4])
+ picType := b[4]
+
+ descDataSplit := dataSplit(b[5:], enc)
+ if len(descDataSplit) != 2 {
+ return nil, errors.New("error decoding PIC description text: invalid encoding")
+ }
+ desc, err := decodeText(enc, descDataSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding PIC description text: %v", err)
+ }
+
+ var mimeType string
+ switch ext {
+ case "jpeg", "jpg":
+ mimeType = "image/jpeg"
+ case "png":
+ mimeType = "image/png"
+ }
+
+ return &Picture{
+ Ext: ext,
+ MIMEType: mimeType,
+ Type: pictureTypes[picType],
+ Description: desc,
+ Data: descDataSplit[1],
+ }, nil
+}
+
+// IDv2.{3,4}
+// -- Header
+// <Header for 'Attached picture', ID: "APIC">
+// -- readAPICFrame
+// Text encoding $xx
+// MIME type <text string> $00
+// Picture type $xx
+// Description <text string according to encoding> $00 (00)
+// Picture data <binary data>
+func readAPICFrame(b []byte) (*Picture, error) {
+ enc := b[0]
+ mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2)
+ mimeType := string(mimeDataSplit[0])
+
+ b = mimeDataSplit[1]
+ if len(b) < 1 {
+ return nil, fmt.Errorf("error decoding APIC mimetype")
+ }
+ picType := b[0]
+
+ descDataSplit := dataSplit(b[1:], enc)
+ if len(descDataSplit) != 2 {
+ return nil, errors.New("error decoding APIC description text: invalid encoding")
+ }
+ desc, err := decodeText(enc, descDataSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding APIC description text: %v", err)
+ }
+
+ var ext string
+ switch mimeType {
+ case "image/jpeg":
+ ext = "jpg"
+ case "image/png":
+ ext = "png"
+ }
+
+ return &Picture{
+ Ext: ext,
+ MIMEType: mimeType,
+ Type: pictureTypes[picType],
+ Description: desc,
+ Data: descDataSplit[1],
+ }, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2metadata.go b/vendor/github.com/dhowden/tag/id3v2metadata.go
new file mode 100644
index 0000000..6185963
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2metadata.go
@@ -0,0 +1,141 @@
+// 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 (
+ "strconv"
+ "strings"
+)
+
+type frameNames map[string][2]string
+
+func (f frameNames) Name(s string, fm Format) string {
+ l, ok := f[s]
+ if !ok {
+ return ""
+ }
+
+ switch fm {
+ case ID3v2_2:
+ return l[0]
+ case ID3v2_3:
+ return l[1]
+ case ID3v2_4:
+ if s == "year" {
+ return "TDRC"
+ }
+ return l[1]
+ }
+ return ""
+}
+
+var frames = frameNames(map[string][2]string{
+ "title": [2]string{"TT2", "TIT2"},
+ "artist": [2]string{"TP1", "TPE1"},
+ "album": [2]string{"TAL", "TALB"},
+ "album_artist": [2]string{"TP2", "TPE2"},
+ "composer": [2]string{"TCM", "TCOM"},
+ "year": [2]string{"TYE", "TYER"},
+ "track": [2]string{"TRK", "TRCK"},
+ "disc": [2]string{"TPA", "TPOS"},
+ "genre": [2]string{"TCO", "TCON"},
+ "picture": [2]string{"PIC", "APIC"},
+ "lyrics": [2]string{"", "USLT"},
+ "comment": [2]string{"COM", "COMM"},
+})
+
+// metadataID3v2 is the implementation of Metadata used for ID3v2 tags.
+type metadataID3v2 struct {
+ header *id3v2Header
+ frames map[string]interface{}
+}
+
+func (m metadataID3v2) getString(k string) string {
+ v, ok := m.frames[k]
+ if !ok {
+ return ""
+ }
+ return v.(string)
+}
+
+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 {
+ return m.getString(frames.Name("title", m.Format()))
+}
+
+func (m metadataID3v2) Artist() string {
+ return m.getString(frames.Name("artist", m.Format()))
+}
+
+func (m metadataID3v2) Album() string {
+ return m.getString(frames.Name("album", m.Format()))
+}
+
+func (m metadataID3v2) AlbumArtist() string {
+ return m.getString(frames.Name("album_artist", m.Format()))
+}
+
+func (m metadataID3v2) Composer() string {
+ return m.getString(frames.Name("composer", m.Format()))
+}
+
+func (m metadataID3v2) Genre() string {
+ return id3v2genre(m.getString(frames.Name("genre", m.Format())))
+}
+
+func (m metadataID3v2) Year() int {
+ year, _ := strconv.Atoi(m.getString(frames.Name("year", m.Format())))
+ return year
+}
+
+func parseXofN(s string) (x, n int) {
+ xn := strings.Split(s, "/")
+ if len(xn) != 2 {
+ x, _ = strconv.Atoi(s)
+ return x, 0
+ }
+ x, _ = strconv.Atoi(strings.TrimSpace(xn[0]))
+ n, _ = strconv.Atoi(strings.TrimSpace(xn[1]))
+ return x, n
+}
+
+func (m metadataID3v2) Track() (int, int) {
+ return parseXofN(m.getString(frames.Name("track", m.Format())))
+}
+
+func (m metadataID3v2) Disc() (int, int) {
+ return parseXofN(m.getString(frames.Name("disc", m.Format())))
+}
+
+func (m metadataID3v2) Lyrics() string {
+ t, ok := m.frames[frames.Name("lyrics", m.Format())]
+ if !ok {
+ return ""
+ }
+ return t.(*Comm).Text
+}
+
+func (m metadataID3v2) Comment() string {
+ t, ok := m.frames[frames.Name("comment", m.Format())]
+ if !ok {
+ return ""
+ }
+ // id3v23 has Text, id3v24 has Description
+ if t.(*Comm).Description == "" {
+ return trimString(t.(*Comm).Text)
+ }
+ return trimString(t.(*Comm).Description)
+}
+
+func (m metadataID3v2) Picture() *Picture {
+ v, ok := m.frames[frames.Name("picture", m.Format())]
+ if !ok {
+ return nil
+ }
+ return v.(*Picture)
+}
diff --git a/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt b/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt
new file mode 100644
index 0000000..f7e01d8
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt
@@ -0,0 +1,3 @@
+All files in this directory are subject to the CC0 1.0 Universal (CC0 1.0)
+Public Domain Dedication license. Its contents can be found at:
+http://creativecommons.org/publicdomain/zero/1.0
diff --git a/vendor/github.com/dhowden/tag/mp4.go b/vendor/github.com/dhowden/tag/mp4.go
new file mode 100644
index 0000000..4c3dd59
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/mp4.go
@@ -0,0 +1,372 @@
+// 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"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+)
+
+var atomTypes = map[int]string{
+ 0: "implicit", // automatic based on atom name
+ 1: "text",
+ 13: "jpeg",
+ 14: "png",
+ 21: "uint8",
+}
+
+// NB: atoms does not include "----", this is handled separately
+var atoms = atomNames(map[string]string{
+ "\xa9alb": "album",
+ "\xa9art": "artist",
+ "\xa9ART": "artist",
+ "aART": "album_artist",
+ "\xa9day": "year",
+ "\xa9nam": "title",
+ "\xa9gen": "genre",
+ "trkn": "track",
+ "\xa9wrt": "composer",
+ "\xa9too": "encoder",
+ "cprt": "copyright",
+ "covr": "picture",
+ "\xa9grp": "grouping",
+ "keyw": "keyword",
+ "\xa9lyr": "lyrics",
+ "\xa9cmt": "comment",
+ "tmpo": "tempo",
+ "cpil": "compilation",
+ "disk": "disc",
+})
+
+// Detect PNG image if "implicit" class is used
+var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
+
+type atomNames map[string]string
+
+func (f atomNames) Name(n string) []string {
+ res := make([]string, 1)
+ for k, v := range f {
+ if v == n {
+ res = append(res, k)
+ }
+ }
+ return res
+}
+
+// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data.
+type metadataMP4 struct {
+ fileType FileType
+ data map[string]interface{}
+}
+
+// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning
+// non-nil error if there was a problem.
+func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
+ m := metadataMP4{
+ data: make(map[string]interface{}),
+ fileType: UnknownFileType,
+ }
+ err := m.readAtoms(r)
+ return m, err
+}
+
+func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
+ for {
+ name, size, err := readAtomHeader(r)
+ if err != nil {
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+
+ switch name {
+ case "meta":
+ // next_item_id (int32)
+ _, err := readBytes(r, 4)
+ if err != nil {
+ return err
+ }
+ fallthrough
+
+ case "moov", "udta", "ilst":
+ return m.readAtoms(r)
+ }
+
+ _, ok := atoms[name]
+ var data []string
+ if name == "----" {
+ name, data, err = readCustomAtom(r, size)
+ if err != nil {
+ return err
+ }
+
+ if name != "----" {
+ ok = true
+ size = 0 // already read data
+ }
+ }
+
+ if !ok {
+ _, err := r.Seek(int64(size-8), io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+
+ err = m.readAtomData(r, name, size-8, data)
+ if err != nil {
+ return err
+ }
+ }
+}
+
+func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error {
+ var b []byte
+ var err error
+ var contentType string
+ if len(processedData) > 0 {
+ b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields
+ contentType = "text"
+ } else {
+ // read the data
+ b, err = readBytes(r, int(size))
+ if err != nil {
+ return err
+ }
+ if len(b) < 8 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 8, len(b))
+ }
+
+ // "data" + size (4 bytes each)
+ b = b[8:]
+
+ if len(b) < 3 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 3, len(b))
+ }
+ class := getInt(b[1:4])
+ var ok bool
+ contentType, ok = atomTypes[class]
+ if !ok {
+ return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
+ }
+
+ // 4: atom version (1 byte) + atom flags (3 bytes)
+ // 4: NULL (usually locale indicator)
+ if len(b) < 8 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b))
+ }
+ b = b[8:]
+ }
+
+ if name == "trkn" || name == "disk" {
+ if len(b) < 6 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b))
+ }
+
+ m.data[name] = int(b[3])
+ m.data[name+"_count"] = int(b[5])
+ return nil
+ }
+
+ if contentType == "implicit" {
+ if name == "covr" {
+ if bytes.HasPrefix(b, pngHeader) {
+ contentType = "png"
+ }
+ // TODO(dhowden): Detect JPEG formats too (harder).
+ }
+ }
+
+ var data interface{}
+ switch contentType {
+ case "implicit":
+ if _, ok := atoms[name]; ok {
+ return fmt.Errorf("unhandled implicit content type for required atom: %q", name)
+ }
+ return nil
+
+ case "text":
+ data = string(b)
+
+ case "uint8":
+ if len(b) < 1 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
+ }
+ data = getInt(b[:1])
+
+ case "jpeg", "png":
+ data = &Picture{
+ Ext: contentType,
+ MIMEType: "image/" + contentType,
+ Data: b,
+ }
+ }
+ m.data[name] = data
+
+ return nil
+}
+
+func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
+ err = binary.Read(r, binary.BigEndian, &size)
+ if err != nil {
+ return
+ }
+ name, err = readString(r, 4)
+ return
+}
+
+// Generic atom.
+// Should have 3 sub atoms : mean, name and data.
+// We check that mean is "com.apple.iTunes" and we use the subname as
+// the name, and move to the data atom.
+// Data atom could have multiple data values, each with a header.
+// If anything goes wrong, we jump at the end of the "----" atom.
+func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) {
+ subNames := make(map[string]string)
+
+ for size > 8 {
+ subName, subSize, err := readAtomHeader(r)
+ if err != nil {
+ return "", nil, err
+ }
+
+ // Remove the size of the atom from the size counter
+ if size >= subSize {
+ size -= subSize
+ } else {
+ return "", nil, errors.New("--- invalid size")
+ }
+
+ b, err := readBytes(r, int(subSize-8))
+ if err != nil {
+ return "", nil, err
+ }
+
+ if len(b) < 4 {
+ return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b))
+ }
+ switch subName {
+ case "mean", "name":
+ subNames[subName] = string(b[4:])
+ case "data":
+ data = append(data, string(b[4:]))
+ }
+ }
+
+ // there should remain only the header size
+ if size != 8 {
+ err := errors.New("---- atom out of bounds")
+ return "", nil, err
+ }
+
+ if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || len(data) == 0 {
+ return "----", nil, nil
+ }
+ return subNames["name"], data, nil
+}
+
+func (metadataMP4) Format() Format { return MP4 }
+func (m metadataMP4) FileType() FileType { return m.fileType }
+
+func (m metadataMP4) Raw() map[string]interface{} { return m.data }
+
+func (m metadataMP4) getString(n []string) string {
+ for _, k := range n {
+ if x, ok := m.data[k]; ok {
+ return x.(string)
+ }
+ }
+ return ""
+}
+
+func (m metadataMP4) getInt(n []string) int {
+ for _, k := range n {
+ if x, ok := m.data[k]; ok {
+ return x.(int)
+ }
+ }
+ return 0
+}
+
+func (m metadataMP4) Title() string {
+ return m.getString(atoms.Name("title"))
+}
+
+func (m metadataMP4) Artist() string {
+ return m.getString(atoms.Name("artist"))
+}
+
+func (m metadataMP4) Album() string {
+ return m.getString(atoms.Name("album"))
+}
+
+func (m metadataMP4) AlbumArtist() string {
+ return m.getString(atoms.Name("album_artist"))
+}
+
+func (m metadataMP4) Composer() string {
+ return m.getString(atoms.Name("composer"))
+}
+
+func (m metadataMP4) Genre() string {
+ return m.getString(atoms.Name("genre"))
+}
+
+func (m metadataMP4) Year() int {
+ date := m.getString(atoms.Name("year"))
+ if len(date) >= 4 {
+ year, _ := strconv.Atoi(date[:4])
+ return year
+ }
+ return 0
+}
+
+func (m metadataMP4) Track() (int, int) {
+ x := m.getInt([]string{"trkn"})
+ if n, ok := m.data["trkn_count"]; ok {
+ return x, n.(int)
+ }
+ return x, 0
+}
+
+func (m metadataMP4) Disc() (int, int) {
+ x := m.getInt([]string{"disk"})
+ if n, ok := m.data["disk_count"]; ok {
+ return x, n.(int)
+ }
+ return x, 0
+}
+
+func (m metadataMP4) Lyrics() string {
+ t, ok := m.data["\xa9lyr"]
+ if !ok {
+ return ""
+ }
+ return t.(string)
+}
+
+func (m metadataMP4) Comment() string {
+ t, ok := m.data["\xa9cmt"]
+ if !ok {
+ return ""
+ }
+ return t.(string)
+}
+
+func (m metadataMP4) Picture() *Picture {
+ v, ok := m.data["covr"]
+ if !ok {
+ return nil
+ }
+ p, _ := v.(*Picture)
+ return p
+}
diff --git a/vendor/github.com/dhowden/tag/ogg.go b/vendor/github.com/dhowden/tag/ogg.go
new file mode 100644
index 0000000..bc26d4a
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/ogg.go
@@ -0,0 +1,119 @@
+// 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"
+ "io"
+)
+
+const (
+ idType int = 1
+ commentType int = 3
+)
+
+// ReadOGGTags reads OGG metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+// See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html
+// and http://www.xiph.org/ogg/doc/framing.html for details.
+func ReadOGGTags(r io.ReadSeeker) (Metadata, error) {
+ oggs, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+ if oggs != "OggS" {
+ return nil, errors.New("expected 'OggS'")
+ }
+
+ // Skip 22 bytes of Page header to read page_segments length byte at position 26
+ // See http://www.xiph.org/ogg/doc/framing.html
+ _, err = r.Seek(22, io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ nS, err := readInt(r, 1)
+ if err != nil {
+ return nil, err
+ }
+
+ // Seek and discard the segments
+ _, err = r.Seek(int64(nS), io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ // First packet type is identification, type 1
+ t, err := readInt(r, 1)
+ if err != nil {
+ return nil, err
+ }
+ if t != idType {
+ return nil, errors.New("expected 'vorbis' identification type 1")
+ }
+
+ // Seek and discard 29 bytes from common and identification header
+ // See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
+ _, err = r.Seek(29, io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ // Beginning of a new page. Comment packet is on a separate page
+ // See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-132000A.2
+ oggs, err = readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+ if oggs != "OggS" {
+ return nil, errors.New("expected 'OggS'")
+ }
+
+ // Skip page 2 header, same as line 30
+ _, err = r.Seek(22, io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ nS, err = readInt(r, 1)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = r.Seek(int64(nS), io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ // Packet type is comment, type 3
+ t, err = readInt(r, 1)
+ if err != nil {
+ return nil, err
+ }
+ if t != commentType {
+ return nil, errors.New("expected 'vorbis' comment type 3")
+ }
+
+ // Seek and discard 6 bytes from common header
+ _, err = r.Seek(6, io.SeekCurrent)
+ if err != nil {
+ return nil, err
+ }
+
+ m := &metadataOGG{
+ newMetadataVorbis(),
+ }
+
+ err = m.readVorbisComment(r)
+ return m, err
+}
+
+type metadataOGG struct {
+ *metadataVorbis
+}
+
+func (m *metadataOGG) FileType() FileType {
+ return OGG
+}
diff --git a/vendor/github.com/dhowden/tag/sum.go b/vendor/github.com/dhowden/tag/sum.go
new file mode 100644
index 0000000..97b27a7
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/sum.go
@@ -0,0 +1,219 @@
+package tag
+
+import (
+ "crypto/sha1"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+)
+
+// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
+// (ID3, MP4) invariant.
+func Sum(r io.ReadSeeker) (string, error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return "", err
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("could not seek back to original position: %v", err)
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return SumFLAC(r)
+
+ case string(b[4:11]) == "ftypM4A":
+ return SumAtoms(r)
+
+ case string(b[0:3]) == "ID3":
+ return SumID3v2(r)
+ }
+
+ h, err := SumID3v1(r)
+ if err != nil {
+ if err == ErrNotID3v1 {
+ return SumAll(r)
+ }
+ return "", err
+ }
+ return h, nil
+}
+
+// SumAll returns a checksum of the content from the reader (until EOF).
+func SumAll(r io.ReadSeeker) (string, error) {
+ h := sha1.New()
+ _, err := io.Copy(h, r)
+ if err != nil {
+ return "", nil
+ }
+ return hashSum(h), nil
+}
+
+// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
+// metadata invariant.
+func SumAtoms(r io.ReadSeeker) (string, error) {
+ for {
+ var size uint32
+ err := binary.Read(r, binary.BigEndian, &size)
+ if err != nil {
+ if err == io.EOF {
+ return "", fmt.Errorf("reached EOF before audio data")
+ }
+ return "", err
+ }
+
+ name, err := readString(r, 4)
+ if err != nil {
+ return "", err
+ }
+
+ switch name {
+ case "meta":
+ // next_item_id (int32)
+ _, err := r.Seek(4, io.SeekCurrent)
+ if err != nil {
+ return "", err
+ }
+ fallthrough
+
+ case "moov", "udta", "ilst":
+ continue
+
+ case "mdat": // stop when we get to the data
+ h := sha1.New()
+ _, err := io.CopyN(h, r, int64(size-8))
+ if err != nil {
+ return "", fmt.Errorf("error reading audio data: %v", err)
+ }
+ return hashSum(h), nil
+ }
+
+ _, err = r.Seek(int64(size-8), io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
+ }
+ }
+}
+
+func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) {
+ n, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err)
+ }
+
+ _, err = r.Seek(-n, io.SeekCurrent)
+ if err != nil {
+ return 0, fmt.Errorf("error seeking back to original position: %v", err)
+ }
+ return n, nil
+}
+
+// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
+// by the io.ReadSeeker which is metadata invariant.
+func SumID3v1(r io.ReadSeeker) (string, error) {
+ n, err := sizeToEndOffset(r, 128)
+ if err != nil {
+ return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+ }
+
+ // TODO: improve this check???
+ if n <= 0 {
+ return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
+ }
+
+ h := sha1.New()
+ _, err = io.CopyN(h, r, n)
+ if err != nil {
+ return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+ }
+ return hashSum(h), nil
+}
+
+// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
+// io.ReadSeeker which is metadata invariant.
+func SumID3v2(r io.ReadSeeker) (string, error) {
+ header, _, err := readID3v2Header(r)
+ if err != nil {
+ return "", fmt.Errorf("error reading ID3v2 header: %v", err)
+ }
+
+ _, err = r.Seek(int64(header.Size), io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
+ }
+
+ n, err := sizeToEndOffset(r, 128)
+ if err != nil {
+ return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+ }
+
+ // TODO: remove this check?????
+ if n < 0 {
+ return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
+ }
+
+ h := sha1.New()
+ _, err = io.CopyN(h, r, n)
+ if err != nil {
+ return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+ }
+ return hashSum(h), nil
+}
+
+// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores
+// metadata fields).
+func SumFLAC(r io.ReadSeeker) (string, error) {
+ flac, err := readString(r, 4)
+ if err != nil {
+ return "", err
+ }
+ if flac != "fLaC" {
+ return "", errors.New("expected 'fLaC'")
+ }
+
+ for {
+ last, err := skipFLACMetadataBlock(r)
+ if err != nil {
+ return "", err
+ }
+
+ if last {
+ break
+ }
+ }
+
+ h := sha1.New()
+ _, err = io.Copy(h, r)
+ if err != nil {
+ return "", fmt.Errorf("error reading data bytes from FLAC: %v", err)
+ }
+ return hashSum(h), nil
+}
+
+func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+ blockHeader, err := readBytes(r, 1)
+ if err != nil {
+ return
+ }
+
+ if getBit(blockHeader[0], 7) {
+ blockHeader[0] ^= (1 << 7)
+ last = true
+ }
+
+ blockLen, err := readInt(r, 3)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(int64(blockLen), io.SeekCurrent)
+ return
+}
+
+func hashSum(h hash.Hash) string {
+ return fmt.Sprintf("%x", h.Sum([]byte{}))
+}
diff --git a/vendor/github.com/dhowden/tag/tag.go b/vendor/github.com/dhowden/tag/tag.go
new file mode 100644
index 0000000..306f1d7
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/tag.go
@@ -0,0 +1,147 @@
+// 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 provides MP3 (ID3: v1, 2.2, 2.3 and 2.4), MP4, FLAC and OGG metadata detection,
+// parsing and artwork extraction.
+//
+// Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File):
+// m, err := tag.ReadFrom(f)
+// if err != nil {
+// log.Fatal(err)
+// }
+// log.Print(m.Format()) // The detected format.
+// log.Print(m.Title()) // The title of the track (see Metadata interface for more details).
+package tag
+
+import (
+ "errors"
+ "fmt"
+ "io"
+)
+
+// ErrNoTagsFound is the error returned by ReadFrom when the metadata format
+// cannot be identified.
+var ErrNoTagsFound = errors.New("no tags found")
+
+// ReadFrom detects and parses audio file metadata tags (currently supports ID3v1,2.{2,3,4}, MP4, FLAC/OGG).
+// Returns non-nil error if the format of the given data could not be determined, or if there was a problem
+// parsing the data.
+func ReadFrom(r io.ReadSeeker) (Metadata, error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ return nil, fmt.Errorf("could not seek back to original position: %v", err)
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return ReadFLACTags(r)
+
+ case string(b[0:4]) == "OggS":
+ return ReadOGGTags(r)
+
+ case string(b[4:8]) == "ftyp":
+ return ReadAtoms(r)
+
+ case string(b[0:3]) == "ID3":
+ return ReadID3v2Tags(r)
+
+ case string(b[0:4]) == "DSD ":
+ return ReadDSFTags(r)
+ }
+
+ m, err := ReadID3v1Tags(r)
+ if err != nil {
+ if err == ErrNotID3v1 {
+ err = ErrNoTagsFound
+ }
+ return nil, err
+ }
+ return m, nil
+}
+
+// Format is an enumeration of metadata types supported by this package.
+type Format string
+
+// Supported tag formats.
+const (
+ UnknownFormat Format = "" // Unknown Format.
+ ID3v1 Format = "ID3v1" // ID3v1 tag format.
+ ID3v2_2 Format = "ID3v2.2" // ID3v2.2 tag format.
+ ID3v2_3 Format = "ID3v2.3" // ID3v2.3 tag format (most common).
+ ID3v2_4 Format = "ID3v2.4" // ID3v2.4 tag format.
+ MP4 Format = "MP4" // MP4 tag (atom) format (see http://www.ftyps.com/ for a full file type list)
+ VORBIS Format = "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
+
+// Supported file types.
+const (
+ UnknownFileType FileType = "" // Unknown FileType.
+ MP3 FileType = "MP3" // MP3 file
+ M4A FileType = "M4A" // M4A file Apple iTunes (ACC) Audio
+ M4B FileType = "M4B" // M4A file Apple iTunes (ACC) Audio Book
+ M4P FileType = "M4P" // M4A file Apple iTunes (ACC) AES Protected Audio
+ ALAC FileType = "ALAC" // Apple Lossless file FIXME: actually detect this
+ FLAC FileType = "FLAC" // FLAC file
+ OGG FileType = "OGG" // OGG file
+ DSF FileType = "DSF" // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf
+)
+
+// Metadata is an interface which is used to describe metadata retrieved by this package.
+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
+
+ // Album returns the album name of the track.
+ Album() string
+
+ // Artist returns the artist name of the track.
+ Artist() string
+
+ // AlbumArtist returns the album artist name of the track.
+ AlbumArtist() string
+
+ // Composer returns the composer of the track.
+ Composer() string
+
+ // Year returns the year of the track.
+ Year() int
+
+ // Genre returns the genre of the track.
+ Genre() string
+
+ // Track returns the track number and total tracks, or zero values if unavailable.
+ Track() (int, int)
+
+ // Disc returns the disc number and total discs, or zero values if unavailable.
+ Disc() (int, int)
+
+ // Picture returns a picture, or nil if not available.
+ Picture() *Picture
+
+ // Lyrics returns the lyrics, or an empty string if unavailable.
+ Lyrics() string
+
+ // Comment returns the comment, or an empty string if unavailable.
+ Comment() string
+
+ // Raw returns the raw mapping of retrieved tag names and associated values.
+ // NB: tag/atom names are not standardised between formats.
+ Raw() map[string]interface{}
+}
diff --git a/vendor/github.com/dhowden/tag/util.go b/vendor/github.com/dhowden/tag/util.go
new file mode 100644
index 0000000..ff9c4f1
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/util.go
@@ -0,0 +1,81 @@
+// 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 (
+ "encoding/binary"
+ "io"
+)
+
+func getBit(b byte, n uint) bool {
+ x := byte(1 << n)
+ return (b & x) == x
+}
+
+func get7BitChunkedInt(b []byte) int {
+ var n int
+ for _, x := range b {
+ n = n << 7
+ n |= int(x)
+ }
+ return n
+}
+
+func getInt(b []byte) int {
+ var n int
+ for _, x := range b {
+ n = n << 8
+ n |= int(x)
+ }
+ return n
+}
+
+func getIntLittleEndian(b []byte) int {
+ var n int
+ for i := len(b) - 1; i >= 0; i-- {
+ n = n << 8
+ n |= int(b[i])
+ }
+ return n
+}
+
+func readBytes(r io.Reader, n int) ([]byte, error) {
+ b := make([]byte, n)
+ _, err := io.ReadFull(r, b)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+func readString(r io.Reader, n int) (string, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+func readInt(r io.Reader, n int) (int, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return 0, err
+ }
+ return getInt(b), nil
+}
+
+func read7BitChunkedInt(r io.Reader, n int) (int, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return 0, err
+ }
+ 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
+}
diff --git a/vendor/github.com/dhowden/tag/vorbis.go b/vendor/github.com/dhowden/tag/vorbis.go
new file mode 100644
index 0000000..9f5ecb8
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/vorbis.go
@@ -0,0 +1,255 @@
+// 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"
+ "time"
+)
+
+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
+ }
+
+ if vendorLen < 0 {
+ return fmt.Errorf("invalid encoding: expected positive length, got %d", vendorLen)
+ }
+
+ 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 VORBIS
+}
+
+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 actually included in the standard, though
+ // it is commonly assigned to albumartist.
+ return m.c["albumartist"]
+}
+
+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 {
+ var dateFormat string
+
+ // The date need to follow the international standard https://en.wikipedia.org/wiki/ISO_8601
+ // and obviously the VorbisComment standard https://wiki.xiph.org/VorbisComment#Date_and_time
+ switch len(m.c["date"]) {
+ case 0:
+ return 0
+ case 4:
+ dateFormat = "2006"
+ case 7:
+ dateFormat = "2006-01"
+ case 10:
+ dateFormat = "2006-01-02"
+ }
+
+ t, _ := time.Parse(dateFormat, m.c["date"])
+ return t.Year()
+}
+
+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) Comment() string {
+ if m.c["comment"] != "" {
+ return m.c["comment"]
+ }
+ return m.c["description"]
+}
+
+func (m *metadataVorbis) Picture() *Picture {
+ return m.p
+}