add licence
This commit is contained in:
@@ -36,15 +36,19 @@ func main() {
|
|||||||
cont.CheckParameters,
|
cont.CheckParameters,
|
||||||
)
|
)
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/rest/ping", withWare(cont.Ping))
|
||||||
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
|
mux.HandleFunc("/rest/ping.view", withWare(cont.Ping))
|
||||||
mux.HandleFunc("/rest/getIndexes.view", withWare(cont.GetIndexes))
|
mux.HandleFunc("/rest/stream", withWare(cont.Stream))
|
||||||
mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
|
|
||||||
mux.HandleFunc("/rest/getCoverArt.view", withWare(cont.GetCoverArt))
|
|
||||||
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
|
mux.HandleFunc("/rest/stream.view", withWare(cont.Stream))
|
||||||
mux.HandleFunc("/rest/getMusicFolders.view", withWare(cont.GetMusicFolders))
|
mux.HandleFunc("/rest/getMusicDirectory", withWare(cont.GetMusicDirectory))
|
||||||
mux.HandleFunc("/rest/getPlaylists.view", withWare(cont.GetPlaylists))
|
mux.HandleFunc("/rest/getMusicDirectory.view", withWare(cont.GetMusicDirectory))
|
||||||
mux.HandleFunc("/rest/getGenres.view", withWare(cont.GetGenres))
|
mux.HandleFunc("/rest/getCoverArt", withWare(cont.GetCoverArt))
|
||||||
mux.HandleFunc("/rest/getPodcasts.view", withWare(cont.GetPodcasts))
|
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{
|
server := &http.Server{
|
||||||
Addr: address,
|
Addr: address,
|
||||||
Handler: mux,
|
Handler: mux,
|
||||||
|
|||||||
2
go.sum
2
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=
|
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.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
|
||||||
git.apache.org/thrift.git v0.12.0/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/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/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=
|
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/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 h1:ycpSqVon/QJJoaT1t8sae0tp1Stg21j+dyuS7OoagcA=
|
||||||
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
|
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/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/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=
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ func respondRaw(w http.ResponseWriter, r *http.Request, code int, sub *subsonic.
|
|||||||
w.Write([]byte(`{"subsonic-response":`))
|
w.Write([]byte(`{"subsonic-response":`))
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
w.Write([]byte("}"))
|
w.Write([]byte("}"))
|
||||||
|
fmt.Println("THE JSON", string(data))
|
||||||
case "jsonp":
|
case "jsonp":
|
||||||
w.Header().Set("Content-Type", "application/javascript")
|
w.Header().Set("Content-Type", "application/javascript")
|
||||||
data, err := json.Marshal(sub)
|
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([]byte(fmt.Sprintf(`%s({"subsonic-response":`, callback)))
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
w.Write([]byte("});"))
|
w.Write([]byte("});"))
|
||||||
|
fmt.Println("THE JSONP", string(data))
|
||||||
default:
|
default:
|
||||||
w.Header().Set("Content-Type", "application/xml")
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
data, err := xml.Marshal(sub)
|
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)
|
log.Printf("could not marshall to xml: %v\n", err)
|
||||||
}
|
}
|
||||||
w.Write(data)
|
w.Write(data)
|
||||||
|
fmt.Println("THE XML", string(data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -167,14 +166,17 @@ func (c *Controller) Stream(w http.ResponseWriter, req *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
stat, _ := file.Stat()
|
stat, _ := file.Stat()
|
||||||
size := strconv.FormatInt(stat.Size(), 10)
|
http.ServeContent(w, req, track.Path, stat.ModTime(), file)
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Header().Set("Content-Length", size)
|
|
||||||
file.Seek(0, 0)
|
|
||||||
io.Copy(w, file)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) GetMusicFolders(w http.ResponseWriter, req *http.Request) {}
|
func (c *Controller) GetLicence(w http.ResponseWriter, req *http.Request) {
|
||||||
func (c *Controller) GetPlaylists(w http.ResponseWriter, req *http.Request) {}
|
sub := subsonic.NewResponse()
|
||||||
func (c *Controller) GetGenres(w http.ResponseWriter, req *http.Request) {}
|
sub.Licence = &subsonic.Licence{
|
||||||
func (c *Controller) GetPodcasts(w http.ResponseWriter, req *http.Request) {}
|
Valid: true,
|
||||||
|
}
|
||||||
|
respond(w, req, sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Controller) NotFound(w http.ResponseWriter, req *http.Request) {
|
||||||
|
respondError(w, req, 0, "unknown route")
|
||||||
|
}
|
||||||
|
|||||||
@@ -94,3 +94,8 @@ type Child struct {
|
|||||||
BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
|
BitRate uint `xml:"bitRate,attr,omitempty" json:"bitrate,omitempty"`
|
||||||
Path string `xml:"path,attr,omitempty" json:"path,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"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
apiVersion = "1.16.1"
|
apiVersion = "1.9.0"
|
||||||
xmlns = "http://subsonic.org/restapi"
|
xmlns = "http://subsonic.org/restapi"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,6 +25,7 @@ type Response struct {
|
|||||||
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
Artist *Artist `xml:"artist" json:"artist,omitempty"`
|
||||||
MusicDirectory *Directory `xml:"directory" json:"directory,omitempty"`
|
MusicDirectory *Directory `xml:"directory" json:"directory,omitempty"`
|
||||||
RandomSongs *RandomSongs `xml:"randomSongs" json:"randomSongs,omitempty"`
|
RandomSongs *RandomSongs `xml:"randomSongs" json:"randomSongs,omitempty"`
|
||||||
|
Licence *Licence `xml:"license" json:"license,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Error struct {
|
type Error struct {
|
||||||
|
|||||||
@@ -188,13 +188,13 @@ func Read(filename string) (Metadata, error) {
|
|||||||
)
|
)
|
||||||
probe, err := command.Output()
|
probe, err := command.Output()
|
||||||
if err != nil {
|
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)
|
filename, err)
|
||||||
}
|
}
|
||||||
var data probeData
|
var data probeData
|
||||||
err = json.Unmarshal(probe, &data)
|
err = json.Unmarshal(probe, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("when unmarshalling: %v\n", err)
|
return nil, fmt.Errorf("when unmarshalling: %v", err)
|
||||||
}
|
}
|
||||||
track := Track{
|
track := Track{
|
||||||
format: data.Format,
|
format: data.Format,
|
||||||
|
|||||||
19
vendor/github.com/dhowden/tag/.editorconfig
generated
vendored
Normal file
19
vendor/github.com/dhowden/tag/.editorconfig
generated
vendored
Normal file
@@ -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
|
||||||
5
vendor/github.com/dhowden/tag/.travis.yml
generated
vendored
Normal file
5
vendor/github.com/dhowden/tag/.travis.yml
generated
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
language: go
|
||||||
|
|
||||||
|
go:
|
||||||
|
- 1.7
|
||||||
|
- tip
|
||||||
23
vendor/github.com/dhowden/tag/LICENSE
generated
vendored
Normal file
23
vendor/github.com/dhowden/tag/LICENSE
generated
vendored
Normal file
@@ -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.
|
||||||
72
vendor/github.com/dhowden/tag/README.md
generated
vendored
Normal file
72
vendor/github.com/dhowden/tag/README.md
generated
vendored
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# MP3/MP4/OGG/FLAC metadata parsing library
|
||||||
|
[](https://travis-ci.org/dhowden/tag)
|
||||||
|
[](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
|
||||||
|
```
|
||||||
110
vendor/github.com/dhowden/tag/dsf.go
generated
vendored
Normal file
110
vendor/github.com/dhowden/tag/dsf.go
generated
vendored
Normal file
@@ -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()
|
||||||
|
}
|
||||||
89
vendor/github.com/dhowden/tag/flac.go
generated
vendored
Normal file
89
vendor/github.com/dhowden/tag/flac.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
81
vendor/github.com/dhowden/tag/id.go
generated
vendored
Normal file
81
vendor/github.com/dhowden/tag/id.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
144
vendor/github.com/dhowden/tag/id3v1.go
generated
vendored
Normal file
144
vendor/github.com/dhowden/tag/id3v1.go
generated
vendored
Normal file
@@ -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) }
|
||||||
434
vendor/github.com/dhowden/tag/id3v2.go
generated
vendored
Normal file
434
vendor/github.com/dhowden/tag/id3v2.go
generated
vendored
Normal file
@@ -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)
|
||||||
|
}
|
||||||
638
vendor/github.com/dhowden/tag/id3v2frames.go
generated
vendored
Normal file
638
vendor/github.com/dhowden/tag/id3v2frames.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
141
vendor/github.com/dhowden/tag/id3v2metadata.go
generated
vendored
Normal file
141
vendor/github.com/dhowden/tag/id3v2metadata.go
generated
vendored
Normal file
@@ -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)
|
||||||
|
}
|
||||||
362
vendor/github.com/dhowden/tag/mp4.go
generated
vendored
Normal file
362
vendor/github.com/dhowden/tag/mp4.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
119
vendor/github.com/dhowden/tag/ogg.go
generated
vendored
Normal file
119
vendor/github.com/dhowden/tag/ogg.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
219
vendor/github.com/dhowden/tag/sum.go
generated
vendored
Normal file
219
vendor/github.com/dhowden/tag/sum.go
generated
vendored
Normal file
@@ -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{}))
|
||||||
|
}
|
||||||
147
vendor/github.com/dhowden/tag/tag.go
generated
vendored
Normal file
147
vendor/github.com/dhowden/tag/tag.go
generated
vendored
Normal file
@@ -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{}
|
||||||
|
}
|
||||||
81
vendor/github.com/dhowden/tag/util.go
generated
vendored
Normal file
81
vendor/github.com/dhowden/tag/util.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
255
vendor/github.com/dhowden/tag/vorbis.go
generated
vendored
Normal file
255
vendor/github.com/dhowden/tag/vorbis.go
generated
vendored
Normal file
@@ -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
|
||||||
|
}
|
||||||
2
vendor/modules.txt
vendored
2
vendor/modules.txt
vendored
@@ -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 v1.9.2
|
||||||
github.com/jinzhu/gorm
|
github.com/jinzhu/gorm
|
||||||
github.com/jinzhu/gorm/dialects/sqlite
|
github.com/jinzhu/gorm/dialects/sqlite
|
||||||
|
|||||||
Reference in New Issue
Block a user