diff --git a/cmd/server/main.go b/cmd/server/main.go index 28445bd..361ee52 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -36,15 +36,19 @@ func main() { cont.CheckParameters, ) mux := http.NewServeMux() + mux.HandleFunc("/rest/ping", withWare(cont.Ping)) mux.HandleFunc("/rest/ping.view", withWare(cont.Ping)) - mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes)) - mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory)) - mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt)) + mux.HandleFunc("/rest/stream", withWare(cont.Stream)) mux.HandleFunc("/rest/stream.view", withWare(cont.Stream)) - mux.HandleFunc("/rest/getMusicFolders.view", withWare(cont.GetMusicFolders)) - mux.HandleFunc("/rest/getPlaylists.view", withWare(cont.GetPlaylists)) - mux.HandleFunc("/rest/getGenres.view", withWare(cont.GetGenres)) - mux.HandleFunc("/rest/getPodcasts.view", withWare(cont.GetPodcasts)) + mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory)) + mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory)) + mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt)) + mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt)) + mux.HandleFunc("/rest/getIndexes", withWare(cont.GetIndexes)) + mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes)) + mux.HandleFunc("/rest/getLicense", withWare(cont.GetLicence)) + mux.HandleFunc("/rest/getLicense.view", withWare(cont.GetLicence)) + mux.HandleFunc("/", withWare(cont.NotFound)) server := &http.Server{ Addr: address, Handler: mux, diff --git a/go.sum b/go.sum index 471d4a0..e54c240 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,7 @@ cloud.google.com/go v0.37.1 h1:2kHhTjz+eKEI7tt3Fqf5j3APCq+z9tuY2CzeCIxTo+A= cloud.google.com/go v0.37.1/go.mod h1:SAbnLi6YTSPKSI0dTUEOVLCkyPfKXK8n4ibqiMoj4ok= git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= git.apache.org/thrift.git v0.12.0/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -62,6 +63,7 @@ github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1 github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/karrick/godirwalk v1.8.0 h1:ycpSqVon/QJJoaT1t8sae0tp1Stg21j+dyuS7OoagcA= github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= diff --git a/handler/handler.go b/handler/handler.go index 3eb17f7..56efb2f 100644 --- a/handler/handler.go +++ b/handler/handler.go @@ -27,6 +27,7 @@ func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic. w.Write([]byte(`{"subsonic-response":`)) w.Write(data) w.Write([]byte("}")) + fmt.Println("THE JSON", string(data)) case "jsonp": w.Header().Set("Content-Type", "application/javascript") data, err := json.Marshal(sub) @@ -37,6 +38,7 @@ func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic. w.Write([]byte(fmt.Sprintf(`%s({"subsonic-response":`, callback))) w.Write(data) w.Write([]byte("});")) + fmt.Println("THE JSONP", string(data)) default: w.Header().Set("Content-Type", "application/xml") data, err := xml.Marshal(sub) @@ -44,6 +46,7 @@ func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic. log.Printf("could not marshall to xml: %v\n", err) } w.Write(data) + fmt.Println("THE XML", string(data)) } } diff --git a/handler/media.go b/handler/media.go index 2441eb9..46e9854 100644 --- a/handler/media.go +++ b/handler/media.go @@ -2,7 +2,6 @@ package handler import ( "fmt" - "io" "net/http" "os" "strconv" @@ -167,14 +166,17 @@ func (c *Controller) Stream(w http.ResponseWriter, req *http.Request) { return } stat, _ := file.Stat() - size := strconv.FormatInt(stat.Size(), 10) - w.Header().Set("Content-Type", "application/octet-stream") - w.Header().Set("Content-Length", size) - file.Seek(0, 0) - io.Copy(w, file) + http.ServeContent(w, req, track.Path, stat.ModTime(), file) } -func (c *Controller) GetMusicFolders(w http.ResponseWriter, req *http.Request) {} -func (c *Controller) GetPlaylists(w http.ResponseWriter, req *http.Request) {} -func (c *Controller) GetGenres(w http.ResponseWriter, req *http.Request) {} -func (c *Controller) GetPodcasts(w http.ResponseWriter, req *http.Request) {} +func (c *Controller) GetLicence(w http.ResponseWriter, req *http.Request) { + sub := subsonic.NewResponse() + sub.Licence = &subsonic.Licence{ + Valid: true, + } + respond(w, req, sub) +} + +func (c *Controller) NotFound(w http.ResponseWriter, req *http.Request) { + respondError(w, req, 0, "unknown route") +} diff --git a/subsonic/media.go b/subsonic/media.go index 7d7ad52..8d202b5 100644 --- a/subsonic/media.go +++ b/subsonic/media.go @@ -94,3 +94,8 @@ type Child struct { BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"` Path string `xml:"path,attr,omitempty" json:"path,omitempty"` } + +type Licence struct { + XMLName xml.Name `xml:"license" json:"-"` + Valid bool `xml:"valid,attr,omitempty" json:"valid,omitempty"` +} diff --git a/subsonic/response.go b/subsonic/response.go index f543b68..c088059 100644 --- a/subsonic/response.go +++ b/subsonic/response.go @@ -7,7 +7,7 @@ import ( ) var ( - apiVersion = "1.16.1" + apiVersion = "1.9.0" xmlns = "http://subsonic.org/restapi" ) @@ -25,6 +25,7 @@ type Response struct { Artist *Artist `xml:"artist" json:"artist,omitempty"` MusicDirectory *Directory `xml:"directory" json:"directory,omitempty"` RandomSongs *RandomSongs `xml:"randomSongs" json:"randomSongs,omitempty"` + Licence *Licence `xml:"license" json:"license,omitempty"` } type Error struct { diff --git a/tags/tags.go b/tags/tags.go index 4c92bf8..f254538 100644 --- a/tags/tags.go +++ b/tags/tags.go @@ -188,13 +188,13 @@ func Read(filename string) (Metadata, error) { ) probe, err := command.Output() if err != nil { - return nil, fmt.Errorf("when running ffprobe with `%s`: %v\n", + return nil, fmt.Errorf("when running ffprobe with `%s`: %v", filename, err) } var data probeData err = json.Unmarshal(probe, &data) if err != nil { - return nil, fmt.Errorf("when unmarshalling: %v\n", err) + return nil, fmt.Errorf("when unmarshalling: %v", err) } track := Track{ format: data.Format, 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 +//
+//
+// -- readTextWithDescrFrame(data, true, true) +// Text encoding $xx +// Language $xx xx xx +// Content descriptor $00 (00) +// Lyrics/text +// -- Header +//
+//
+// -- readTextWithDescrFrame(data, false, ) +// Text encoding $xx +// Description $00 (00) +// Value +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 $00 (00) +// Picture 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 +//
+// -- readAPICFrame +// Text encoding $xx +// MIME type $00 +// Picture type $xx +// Description $00 (00) +// Picture 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/mp4.go b/vendor/github.com/dhowden/tag/mp4.go new file mode 100644 index 0000000..3970d91 --- /dev/null +++ b/vendor/github.com/dhowden/tag/mp4.go @@ -0,0 +1,362 @@ +// 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" +) + +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] + if name == "----" { + name, size, err = readCustomAtom(r, size) + if err != nil { + return err + } + + if name != "----" { + ok = true + } + } + + if !ok { + _, err := r.Seek(int64(size-8), io.SeekCurrent) + if err != nil { + return err + } + continue + } + + err = m.readAtomData(r, name, size-8) + if err != nil { + return err + } + } +} + +func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32) error { + 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]) + 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. +// If anything goes wrong, we jump at the end of the "----" atom. +func readCustomAtom(r io.ReadSeeker, size uint32) (string, uint32, error) { + subNames := make(map[string]string) + var dataSize uint32 + + for size > 8 { + subName, subSize, err := readAtomHeader(r) + if err != nil { + return "", 0, err + } + + // Remove the size of the atom from the size counter + size -= subSize + + switch subName { + case "mean", "name": + b, err := readBytes(r, int(subSize-8)) + if err != nil { + return "", 0, err + } + + if len(b) < 4 { + return "", 0, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b)) + } + subNames[subName] = string(b[4:]) + + case "data": + // Found the "data" atom, rewind + dataSize = subSize + 8 // will need to re-read "data" + size (4 + 4) + _, err := r.Seek(-8, io.SeekCurrent) + if err != nil { + return "", 0, err + } + } + } + + // there should remain only the header size + if size != 8 { + err := errors.New("---- atom out of bounds") + return "", 0, err + } + + if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || dataSize == 0 { + return "----", 0, nil + } + return subNames["name"], dataSize, 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 +} diff --git a/vendor/modules.txt b/vendor/modules.txt index b19f9e2..f736e1d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,3 +1,5 @@ +# github.com/dhowden/tag v0.0.0-20181104225729-a9f04c2798ca +github.com/dhowden/tag # github.com/jinzhu/gorm v1.9.2 github.com/jinzhu/gorm github.com/jinzhu/gorm/dialects/sqlite