diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..7bad9dc
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,36 @@
+# Fase 1: Build
+FROM golang:1.21-alpine AS builder
+
+WORKDIR /app
+
+# Copia i file necessari per la compilazione
+COPY go.mod go.sum ./
+RUN go mod download
+
+COPY . .
+
+# Compila l'applicazione
+RUN CGO_ENABLED=0 GOOS=linux go build -o u-pod
+
+# Fase 2: Container leggero
+FROM alpine:latest
+
+WORKDIR /app
+
+# Copia solo l'eseguibile dalla fase di build
+COPY --from=builder /app/u-pod .
+COPY --from=builder /app/cover.jpg .
+RUN mkdir -p audio covers
+
+# Variabili d'ambiente obbligatorie (da sovrascrivere al runtime)
+ENV PODCAST_PORT=":8080" \
+ PODCAST_BASE_URL="http://localhost:8080" \
+ PODCAST_AUDIO_DIR="/app/audio" \
+ PODCAST_COVERS_DIR="/app/covers" \
+ PODCAST_TITLE="Yo mom Podcast"
+
+# Esponi la porta e avvia l'applicazione
+EXPOSE 8080
+VOLUME ["/app/audio", "/app/covers"]
+CMD ["./u-pod"]
+
diff --git a/README.md b/README.md
index b6dcc43..ce3b3f3 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,26 @@
+# U-Pod
+
+## Overview
+
This is U-Pod, the torpedo-style podcast system with minimal footprint and maximum utility.
No databases needed to function, no cloud required, no datacenter necessary - just the filesystem and your content. Even your mom on her knees could help set it up, though that's not a functional requirement.
+## Setup
+
First, you need to build the image. I've included a handy Dockerfile in the folder. Use it.
Once you've created your image, use Docker to run it after setting these environment variables:
+
PODCAST_PORT=:8080
PODCAST_BASE_URL=http://your-domain.com
PODCAST_AUDIO_DIR=/app/data/audio
PODCAST_COVERS_DIR=/app/data/covers
PODCAST_TITLE="My Podcast - and your mom"
+
+
+## File Linking
What links files in /audio to files in /covers?
@@ -28,19 +38,19 @@ U-Pod will then create the torpedo, with the image properly linked to the audio.
Once you have the Docker image, simply create the /data folder and put your files in it.
-FAQ:
+## FAQ
- My "SisterGermanaGangbang 3.0" software can't use it.
-- It uses the standard podcast format http://www.itunes.com/dtds/podcast-1.0.dtd. If your shitty software can't read it, take it up with Sister Germana.
+ - It uses the standard podcast format http://www.itunes.com/dtds/podcast-1.0.dtd. If your shitty software can't read it, take it up with Sister Germana.
- I don't know how to create Docker images.
-- Learn.
+ - Learn.
- What do you mean by environment variable?
-- Die! Die! Die!
+ - Die! Die! Die!
- I need you to change this, this and that.
-- No, No, and No
-
-
+ - No, No, and No
+- How can I get support for this software?
+- You cannot. Code is simple, and you are alone in the night, like any operation engineer during the night. Man up!!!!
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..adf2485
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,5 @@
+module u-pod
+
+go 1.21.0
+
+require github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..c435635
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 h1:OtSeLS5y0Uy01jaKK4mA/WVIYtpzVm63vLVAPzJXigg=
+github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8/go.mod h1:apkPC/CR3s48O2D7Y++n1XWEpgPNNCjXYga3PPbJe2E=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..7547b9e
--- /dev/null
+++ b/main.go
@@ -0,0 +1,72 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+var (
+ port = os.Getenv("PODCAST_PORT") // Es: ":8080"
+ baseURL = os.Getenv("PODCAST_BASE_URL") // Es: "http://mioserver.com"
+ audioDir = os.Getenv("PODCAST_AUDIO_DIR") // Es: "/data/podcast/audio"
+ coversDir = os.Getenv("PODCAST_COVERS_DIR") // Es: "/data/podcast/covers"
+ podTitle = os.Getenv("PODCAST_TITLE") // Es: "Il Mio Podcast"
+)
+
+func init() {
+ // Verifica variabili d'ambiente
+ for _, v := range []struct {
+ name string
+ value string
+ }{
+ {"PODCAST_PORT", port},
+ {"PODCAST_BASE_URL", baseURL},
+ {"PODCAST_AUDIO_DIR", audioDir},
+ {"PODCAST_COVERS_DIR", coversDir},
+ {"PODCAST_TITLE", podTitle},
+ } {
+ if v.value == "" {
+ log.Fatalf("Variabile d'ambiente mancante: %s", v.name)
+ }
+ }
+}
+
+func main() {
+ // Handler principale
+ http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ // Verifica se la richiesta รจ per un file audio o cover
+ requestedFile := filepath.Clean(r.URL.Path)
+ isAudio := strings.HasPrefix(requestedFile, "/audio/")
+ isCover := strings.HasPrefix(requestedFile, "/covers/")
+
+ if isAudio || isCover {
+ // Servi il file richiesto
+ http.ServeFile(w, r, filepath.Join(".", requestedFile))
+ return
+ }
+
+ // Altrimenti servi sempre l'RSS
+ if err := generateRSS(); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ http.ServeFile(w, r, "feed.xml")
+ })
+
+ // Aggiornamento periodico
+ go func() {
+ for {
+ time.Sleep(5 * time.Minute)
+ if err := generateRSS(); err != nil {
+ log.Printf("Aggiornamento RSS fallito: %v", err)
+ }
+ }
+ }()
+
+ log.Printf("Server in ascolto su %s%s", baseURL, port)
+ log.Fatal(http.ListenAndServe(port, nil))
+}
diff --git a/pod.go b/pod.go
new file mode 100644
index 0000000..6de6b37
--- /dev/null
+++ b/pod.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/dhowden/tag"
+)
+
+type Episode struct {
+ Title string
+ File string
+ Cover string
+ PubDate string
+ Size int64
+}
+
+var (
+ lastMod time.Time
+ rssLock sync.Mutex
+)
+
+func generateRSS() error {
+ rssLock.Lock()
+ defer rssLock.Unlock()
+
+ episodes, err := scanEpisodes()
+ if err != nil {
+ return err
+ }
+
+ rss := fmt.Sprintf(`
+
+
+ %s
+ %s`, podTitle, baseURL)
+
+ for _, ep := range episodes {
+ rss += fmt.Sprintf(`
+
+ %s
+
+ %s`,
+ ep.Title, baseURL, ep.File, ep.Size, ep.PubDate)
+
+ if ep.Cover != "" {
+ rss += fmt.Sprintf(`
+ `, baseURL, ep.Cover)
+ }
+
+ rss += "\n "
+ }
+
+ rss += "\n\n"
+
+ return os.WriteFile("feed.xml", []byte(rss), 0644)
+}
+
+func scanEpisodes() ([]Episode, error) {
+ var episodes []Episode
+
+ err := filepath.Walk(audioDir, func(path string, info os.FileInfo, err error) error {
+ if err != nil || info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".mp3") {
+ return nil
+ }
+
+ file, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer file.Close()
+
+ meta, err := tag.ReadFrom(file)
+ if err != nil {
+ log.Printf("File %s senza metadati ID3: %v", path, err)
+ return nil
+ }
+
+ relPath, _ := filepath.Rel(".", path)
+ baseName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
+ coverPath := filepath.Join(coversDir, baseName+".jpg")
+
+ ep := Episode{
+ Title: meta.Title(),
+ File: relPath,
+ PubDate: info.ModTime().Format(time.RFC1123),
+ Size: info.Size(),
+ }
+
+ if _, err := os.Stat(coverPath); err == nil {
+ ep.Cover, _ = filepath.Rel(".", coverPath)
+ }
+
+ episodes = append(episodes, ep)
+ return nil
+ })
+
+ return episodes, err
+}
diff --git a/vendor/github.com/dhowden/tag/.editorconfig b/vendor/github.com/dhowden/tag/.editorconfig
new file mode 100644
index 0000000..57515d0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.editorconfig
@@ -0,0 +1,19 @@
+# EditorConfig helps developers define and maintain consistent
+# coding styles between different editors and IDEs
+# editorconfig.org
+
+root = true
+
+[*.go]
+indent_style = tab
+indent_size = 3
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*]
+# We recommend you to keep these unchanged
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/vendor/github.com/dhowden/tag/.travis.yml b/vendor/github.com/dhowden/tag/.travis.yml
new file mode 100644
index 0000000..3a81bf9
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/.travis.yml
@@ -0,0 +1,5 @@
+language: go
+
+go:
+ - 1.7
+ - tip
\ No newline at end of file
diff --git a/vendor/github.com/dhowden/tag/LICENSE b/vendor/github.com/dhowden/tag/LICENSE
new file mode 100644
index 0000000..dfd760c
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/LICENSE
@@ -0,0 +1,23 @@
+Copyright 2015, David Howden
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ Redistributions of source code must retain the above copyright notice, this
+ list of conditions and the following disclaimer.
+
+ Redistributions in binary form must reproduce the above copyright notice, this
+ list of conditions and the following disclaimer in the documentation and/or
+ other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/vendor/github.com/dhowden/tag/README.md b/vendor/github.com/dhowden/tag/README.md
new file mode 100644
index 0000000..dbc05e0
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/README.md
@@ -0,0 +1,71 @@
+# MP3/MP4/OGG/FLAC metadata parsing library
+[](https://pkg.go.dev/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.
+
+[https://pkg.go.dev/github.com/dhowden/tag#Sum](https://pkg.go.dev/github.com/dhowden/tag#Sum)
+
+## Tools
+
+There are simple command-line tools which demonstrate basic tag extraction and summing:
+
+```console
+$ go install github.com/dhowden/tag/cmd/tag@latest
+$ cd $GOPATH/bin
+$ ./tag 11\ High\ Hopes.m4a
+Metadata Format: MP4
+Title: High Hopes
+Album: The Division Bell
+Artist: Pink Floyd
+Composer: Abbey Road Recording Studios/David Gilmour/Polly Samson
+Year: 1994
+Track: 11 of 11
+Disc: 1 of 1
+Picture: Picture{Ext: jpeg, MIMEType: image/jpeg, Type: , Description: , Data.Size: 606109}
+
+$ ./sum 11\ High\ Hopes.m4a
+2ae208c5f00a1f21f5fac9b7f6e0b8e52c06da29
+```
diff --git a/vendor/github.com/dhowden/tag/dsf.go b/vendor/github.com/dhowden/tag/dsf.go
new file mode 100644
index 0000000..8ca34f2
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/dsf.go
@@ -0,0 +1,109 @@
+// 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
+ }
+
+ id3Pointer, err := readUint64LittleEndian(r)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = r.Seek(int64(id3Pointer), io.SeekStart)
+ if err != nil {
+ return nil, err
+ }
+
+ id3, err := ReadID3v2Tags(r)
+ if err != nil {
+ return nil, err
+ }
+
+ return metadataDSF{id3}, nil
+}
+
+type metadataDSF struct {
+ id3 Metadata
+}
+
+func (m metadataDSF) Format() Format {
+ return m.id3.Format()
+}
+
+func (m metadataDSF) FileType() FileType {
+ return DSF
+}
+
+func (m metadataDSF) Title() string {
+ return m.id3.Title()
+}
+
+func (m metadataDSF) Album() string {
+ return m.id3.Album()
+}
+
+func (m metadataDSF) Artist() string {
+ return m.id3.Artist()
+}
+
+func (m metadataDSF) AlbumArtist() string {
+ return m.id3.AlbumArtist()
+}
+
+func (m metadataDSF) Composer() string {
+ return m.id3.Composer()
+}
+
+func (m metadataDSF) Year() int {
+ return m.id3.Year()
+}
+
+func (m metadataDSF) Genre() string {
+ return m.id3.Genre()
+}
+
+func (m metadataDSF) Track() (int, int) {
+ return m.id3.Track()
+}
+
+func (m metadataDSF) Disc() (int, int) {
+ return m.id3.Disc()
+}
+
+func (m metadataDSF) Picture() *Picture {
+ return m.id3.Picture()
+}
+
+func (m metadataDSF) Lyrics() string {
+ return m.id3.Lyrics()
+}
+
+func (m metadataDSF) Comment() string {
+ return m.id3.Comment()
+}
+
+func (m metadataDSF) Raw() map[string]interface{} {
+ return m.id3.Raw()
+}
diff --git a/vendor/github.com/dhowden/tag/flac.go b/vendor/github.com/dhowden/tag/flac.go
new file mode 100644
index 0000000..c370467
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/flac.go
@@ -0,0 +1,89 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+ "errors"
+ "io"
+)
+
+// blockType is a type which represents an enumeration of valid FLAC blocks
+type blockType byte
+
+// FLAC block types.
+const (
+ // Stream Info Block 0
+ // Padding Block 1
+ // Application Block 2
+ // Seektable Block 3
+ // Cue Sheet Block 5
+ vorbisCommentBlock blockType = 4
+ pictureBlock blockType = 6
+)
+
+// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting
+// metadata in a Metadata implementation, or non-nil error if there was a problem.
+func ReadFLACTags(r io.ReadSeeker) (Metadata, error) {
+ flac, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+ if flac != "fLaC" {
+ return nil, errors.New("expected 'fLaC'")
+ }
+
+ m := &metadataFLAC{
+ newMetadataVorbis(),
+ }
+
+ for {
+ last, err := m.readFLACMetadataBlock(r)
+ if err != nil {
+ return nil, err
+ }
+
+ if last {
+ break
+ }
+ }
+ return m, nil
+}
+
+type metadataFLAC struct {
+ *metadataVorbis
+}
+
+func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+ blockHeader, err := readBytes(r, 1)
+ if err != nil {
+ return
+ }
+
+ if getBit(blockHeader[0], 7) {
+ blockHeader[0] ^= (1 << 7)
+ last = true
+ }
+
+ blockLen, err := readInt(r, 3)
+ if err != nil {
+ return
+ }
+
+ switch blockType(blockHeader[0]) {
+ case vorbisCommentBlock:
+ err = m.readVorbisComment(r)
+
+ case pictureBlock:
+ err = m.readPictureBlock(r)
+
+ default:
+ _, err = r.Seek(int64(blockLen), io.SeekCurrent)
+ }
+ return
+}
+
+func (m *metadataFLAC) FileType() FileType {
+ return FLAC
+}
diff --git a/vendor/github.com/dhowden/tag/id.go b/vendor/github.com/dhowden/tag/id.go
new file mode 100644
index 0000000..2410356
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id.go
@@ -0,0 +1,81 @@
+package tag
+
+import (
+ "fmt"
+ "io"
+)
+
+// Identify identifies the format and file type of the data in the ReadSeeker.
+func Identify(r io.ReadSeeker) (format Format, fileType FileType, err error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ err = fmt.Errorf("could not seek back to original position: %v", err)
+ return
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return VORBIS, FLAC, nil
+
+ case string(b[0:4]) == "OggS":
+ return VORBIS, OGG, nil
+
+ case string(b[4:8]) == "ftyp":
+ b = b[8:11]
+ fileType = UnknownFileType
+ switch string(b) {
+ case "M4A":
+ fileType = M4A
+
+ case "M4B":
+ fileType = M4B
+
+ case "M4P":
+ fileType = M4P
+ }
+ return MP4, fileType, nil
+
+ case string(b[0:3]) == "ID3":
+ b := b[3:]
+ switch uint(b[0]) {
+ case 2:
+ format = ID3v2_2
+ case 3:
+ format = ID3v2_3
+ case 4:
+ format = ID3v2_4
+ case 0, 1:
+ fallthrough
+ default:
+ err = fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0]))
+ return
+ }
+ return format, MP3, nil
+ }
+
+ n, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return
+ }
+
+ tag, err := readString(r, 3)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(-n, io.SeekCurrent)
+ if err != nil {
+ return
+ }
+
+ if tag != "TAG" {
+ err = ErrNoTagsFound
+ return
+ }
+ return ID3v1, MP3, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v1.go b/vendor/github.com/dhowden/tag/id3v1.go
new file mode 100644
index 0000000..0953f0b
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v1.go
@@ -0,0 +1,144 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+ "errors"
+ "io"
+ "strconv"
+ "strings"
+)
+
+// id3v1Genres is a list of genres as given in the ID3v1 specification.
+var id3v1Genres = [...]string{
+ "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge",
+ "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B",
+ "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska",
+ "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient",
+ "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical",
+ "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel",
+ "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative",
+ "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic",
+ "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk",
+ "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta",
+ "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American",
+ "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer",
+ "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro",
+ "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock",
+ "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival",
+ "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock",
+ "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band",
+ "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson",
+ "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus",
+ "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba",
+ "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle",
+ "Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall",
+}
+
+// ErrNotID3v1 is an error which is returned when no ID3v1 header is found.
+var ErrNotID3v1 = errors.New("invalid ID3v1 header")
+
+// ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker. Returns ErrNotID3v1
+// if there are no ID3v1 tags, otherwise non-nil error if there was a problem.
+func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) {
+ _, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return nil, err
+ }
+
+ if tag, err := readString(r, 3); err != nil {
+ return nil, err
+ } else if tag != "TAG" {
+ return nil, ErrNotID3v1
+ }
+
+ title, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ artist, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ album, err := readString(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ year, err := readString(r, 4)
+ if err != nil {
+ return nil, err
+ }
+
+ commentBytes, err := readBytes(r, 30)
+ if err != nil {
+ return nil, err
+ }
+
+ var comment string
+ var track int
+ if commentBytes[28] == 0 {
+ comment = trimString(string(commentBytes[:28]))
+ track = int(commentBytes[29])
+ } else {
+ comment = trimString(string(commentBytes))
+ }
+
+ var genre string
+ genreID, err := readBytes(r, 1)
+ if err != nil {
+ return nil, err
+ }
+ if int(genreID[0]) < len(id3v1Genres) {
+ genre = id3v1Genres[int(genreID[0])]
+ }
+
+ m := make(map[string]interface{})
+ m["title"] = trimString(title)
+ m["artist"] = trimString(artist)
+ m["album"] = trimString(album)
+ m["year"] = trimString(year)
+ m["comment"] = trimString(comment)
+ m["track"] = track
+ m["genre"] = genre
+
+ return metadataID3v1(m), nil
+}
+
+func trimString(x string) string {
+ return strings.TrimSpace(strings.Trim(x, "\x00"))
+}
+
+// metadataID3v1 is the implementation of Metadata used for ID3v1 tags.
+type metadataID3v1 map[string]interface{}
+
+func (metadataID3v1) Format() Format { return ID3v1 }
+func (metadataID3v1) FileType() FileType { return MP3 }
+func (m metadataID3v1) Raw() map[string]interface{} { return m }
+
+func (m metadataID3v1) Title() string { return m["title"].(string) }
+func (m metadataID3v1) Album() string { return m["album"].(string) }
+func (m metadataID3v1) Artist() string { return m["artist"].(string) }
+func (m metadataID3v1) Genre() string { return m["genre"].(string) }
+
+func (m metadataID3v1) Year() int {
+ y := m["year"].(string)
+ n, err := strconv.Atoi(y)
+ if err != nil {
+ return 0
+ }
+ return n
+}
+
+func (m metadataID3v1) Track() (int, int) { return m["track"].(int), 0 }
+
+func (m metadataID3v1) AlbumArtist() string { return "" }
+func (m metadataID3v1) Composer() string { return "" }
+func (metadataID3v1) Disc() (int, int) { return 0, 0 }
+func (m metadataID3v1) Picture() *Picture { return nil }
+func (m metadataID3v1) Lyrics() string { return "" }
+func (m metadataID3v1) Comment() string { return m["comment"].(string) }
diff --git a/vendor/github.com/dhowden/tag/id3v2.go b/vendor/github.com/dhowden/tag/id3v2.go
new file mode 100644
index 0000000..fe33685
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2.go
@@ -0,0 +1,469 @@
+// 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"
+ "regexp"
+ "strconv"
+ "strings"
+)
+
+// Genre definitions 0-79 follow the ID3 tag specification of 1999.
+// More genres have been successively introduced in later Winamp versions.
+// https://en.wikipedia.org/wiki/List_of_ID3v1_genres#Extension_by_Winamp
+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",
+ "Christmas", "Art Rock", "Baroque", "Bhangra", "Big Beat", "Breakbeat",
+ "Chillout", "Downtempo", "Dub", "EBM", "Eclectic", "Electro",
+ "Electroclash", "Emo", "Experimental", "Garage", "Global", "IDM",
+ "Illbient", "Industro-Goth", "Jam Band", "Krautrock", "Leftfield", "Lounge",
+ "Math Rock", "New Romantic", "Nu-Breakz", "Post-Punk", "Post-Rock", "Psytrance",
+ "Shoegaze", "Space Rock", "Trop Rock", "World Music", "Neoclassical", "Audiobook",
+ "Audio Theatre", "Neue Deutsche Welle", "Podcast", "Indie Rock", "G-Funk", "Dubstep",
+ "Garage Rock", "Psybient",
+}
+
+// id3v2Header is a type which represents an ID3v2 tag header.
+type id3v2Header struct {
+ Version Format
+ Unsynchronisation bool
+ ExtendedHeader bool
+ Experimental bool
+ Size uint
+}
+
+// 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 uint, 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: uint(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 := uint(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 := uint(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 uint, headerSize uint, err error) {
+ name, err = readString(r, 3)
+ if err != nil {
+ return
+ }
+ size, err = readUint(r, 3)
+ if err != nil {
+ return
+ }
+ headerSize = 6
+ return
+}
+
+func readID3v2_3FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
+ name, err = readString(r, 4)
+ if err != nil {
+ return
+ }
+ size, err = readUint(r, 4)
+ if err != nil {
+ return
+ }
+ headerSize = 8
+ return
+}
+
+func readID3v2_4FrameHeader(r io.Reader) (name string, size uint, headerSize uint, err error) {
+ name, err = readString(r, 4)
+ if err != nil {
+ return
+ }
+ size, err = read7BitChunkedUint(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 uint, h *id3v2Header) (map[string]interface{}, error) {
+ result := make(map[string]interface{})
+
+ for offset < h.Size {
+ var err error
+ var name string
+ var size, headerSize uint
+ 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 {
+ switch h.Version {
+ case ID3v2_3:
+ // No data length indicator defined.
+ if _, err := read7BitChunkedUint(r, 4); err != nil { // read 4
+ return nil, err
+ }
+ size -= 4
+
+ case ID3v2_4:
+ // Must have a data length indicator (to give the size) if compression is enabled.
+ if !flags.DataLengthIndicator {
+ return nil, errors.New("compression without data length indicator")
+ }
+
+ default:
+ return nil, fmt.Errorf("unsupported compression flag used in %v", h.Version)
+ }
+ }
+
+ if flags.DataLengthIndicator {
+ if h.Version == ID3v2_3 {
+ return nil, fmt.Errorf("data length indicator set but not defined for %v", ID3v2_3)
+ }
+
+ size, err = read7BitChunkedUint(r, 4)
+ if err != nil { // read 4
+ return nil, err
+ }
+ }
+
+ if flags.Encryption {
+ _, err = readBytes(r, 1) // read 1 byte of encryption method
+ if err != nil {
+ return nil, err
+ }
+ size--
+ }
+ }
+
+ 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, fmt.Errorf("could not read %q (%q): %v", name, rawName, err)
+ }
+ result[rawName] = t
+
+ case name == "APIC":
+ p, err := readAPICFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = p
+
+ case name == "PIC":
+ p, err := readPICFrame(b)
+ if err != nil {
+ return nil, err
+ }
+ result[rawName] = p
+
+ default:
+ result[rawName] = b
+ }
+ }
+ return result, nil
+}
+
+type unsynchroniser struct {
+ io.Reader
+ ff bool
+}
+
+// filter io.Reader which skip the Unsynchronisation bytes
+func (r *unsynchroniser) Read(p []byte) (int, error) {
+ b := make([]byte, 1)
+ i := 0
+ for i < len(p) {
+ if n, err := r.Reader.Read(b); err != nil || n == 0 {
+ return i, err
+ }
+ if r.ff && b[0] == 0x00 {
+ r.ff = false
+ continue
+ }
+ p[i] = b[0]
+ i++
+ r.ff = (b[0] == 0xFF)
+ }
+ return i, nil
+}
+
+// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning
+// non-nil error on failure.
+func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) {
+ h, offset, err := readID3v2Header(r)
+ if err != nil {
+ return nil, err
+ }
+
+ var ur io.Reader = r
+ if h.Unsynchronisation {
+ ur = &unsynchroniser{Reader: r}
+ }
+
+ f, err := readID3v2Frames(ur, offset, h)
+ if err != nil {
+ return nil, err
+ }
+ return metadataID3v2{header: h, frames: f}, nil
+}
+
+var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`)
+
+// id3v2genre parse a id3v2 genre tag and expand the numeric genres
+func id3v2genre(genre string) string {
+ c := true
+ for c {
+ orig := genre
+ if match := id3v2genreRe.FindStringSubmatch(genre); len(match) > 0 {
+ if genreID, err := strconv.Atoi(match[2]); err == nil {
+ if genreID < len(id3v2Genres) {
+ genre = id3v2Genres[genreID]
+ if match[1] != "" {
+ genre = strings.TrimSpace(match[1]) + " " + genre
+ }
+ if match[3] != "" {
+ genre = genre + " " + match[3]
+ }
+ }
+ }
+ }
+ c = (orig != genre)
+ }
+ return strings.Replace(genre, "((", "(", -1)
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2frames.go b/vendor/github.com/dhowden/tag/id3v2frames.go
new file mode 100644
index 0000000..8397eba
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2frames.go
@@ -0,0 +1,655 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "strings"
+ "unicode/utf16"
+)
+
+// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding
+// is specified without a corresponding BOM in the data.
+var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian
+
+// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4).
+var id3v22Frames = map[string]string{
+ "BUF": "Recommended buffer size",
+
+ "CNT": "Play counter",
+ "COM": "Comments",
+ "CRA": "Audio encryption",
+ "CRM": "Encrypted meta frame",
+
+ "ETC": "Event timing codes",
+ "EQU": "Equalization",
+
+ "GEO": "General encapsulated object",
+
+ "IPL": "Involved people list",
+
+ "LNK": "Linked information",
+
+ "MCI": "Music CD Identifier",
+ "MLL": "MPEG location lookup table",
+
+ "PIC": "Attached picture",
+ "POP": "Popularimeter",
+
+ "REV": "Reverb",
+ "RVA": "Relative volume adjustment",
+
+ "SLT": "Synchronized lyric/text",
+ "STC": "Synced tempo codes",
+
+ "TAL": "Album/Movie/Show title",
+ "TBP": "BPM (Beats Per Minute)",
+ "TCM": "Composer",
+ "TCO": "Content type",
+ "TCR": "Copyright message",
+ "TDA": "Date",
+ "TDY": "Playlist delay",
+ "TEN": "Encoded by",
+ "TFT": "File type",
+ "TIM": "Time",
+ "TKE": "Initial key",
+ "TLA": "Language(s)",
+ "TLE": "Length",
+ "TMT": "Media type",
+ "TOA": "Original artist(s)/performer(s)",
+ "TOF": "Original filename",
+ "TOL": "Original Lyricist(s)/text writer(s)",
+ "TOR": "Original release year",
+ "TOT": "Original album/Movie/Show title",
+ "TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group",
+ "TP2": "Band/Orchestra/Accompaniment",
+ "TP3": "Conductor/Performer refinement",
+ "TP4": "Interpreted, remixed, or otherwise modified by",
+ "TPA": "Part of a set",
+ "TPB": "Publisher",
+ "TRC": "ISRC (International Standard Recording Code)",
+ "TRD": "Recording dates",
+ "TRK": "Track number/Position in set",
+ "TSI": "Size",
+ "TSS": "Software/hardware and settings used for encoding",
+ "TT1": "Content group description",
+ "TT2": "Title/Songname/Content description",
+ "TT3": "Subtitle/Description refinement",
+ "TXT": "Lyricist/text writer",
+ "TXX": "User defined text information frame",
+ "TYE": "Year",
+
+ "UFI": "Unique file identifier",
+ "ULT": "Unsychronized lyric/text transcription",
+
+ "WAF": "Official audio file webpage",
+ "WAR": "Official artist/performer webpage",
+ "WAS": "Official audio source webpage",
+ "WCM": "Commercial information",
+ "WCP": "Copyright/Legal information",
+ "WPB": "Publishers official webpage",
+ "WXX": "User defined URL link frame",
+}
+
+// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames).
+var id3v23Frames = map[string]string{
+ "AENC": "Audio encryption]",
+ "APIC": "Attached picture",
+ "COMM": "Comments",
+ "COMR": "Commercial frame",
+ "ENCR": "Encryption method registration",
+ "EQUA": "Equalization",
+ "ETCO": "Event timing codes",
+ "GEOB": "General encapsulated object",
+ "GRID": "Group identification registration",
+ "IPLS": "Involved people list",
+ "LINK": "Linked information",
+ "MCDI": "Music CD identifier",
+ "MLLT": "MPEG location lookup table",
+ "OWNE": "Ownership frame",
+ "PRIV": "Private frame",
+ "PCNT": "Play counter",
+ "POPM": "Popularimeter",
+ "POSS": "Position synchronisation frame",
+ "RBUF": "Recommended buffer size",
+ "RVAD": "Relative volume adjustment",
+ "RVRB": "Reverb",
+ "SYLT": "Synchronized lyric/text",
+ "SYTC": "Synchronized tempo codes",
+ "TALB": "Album/Movie/Show title",
+ "TBPM": "BPM (beats per minute)",
+ "TCMP": "iTunes Compilation Flag",
+ "TCOM": "Composer",
+ "TCON": "Content type",
+ "TCOP": "Copyright message",
+ "TDAT": "Date",
+ "TDLY": "Playlist delay",
+ "TENC": "Encoded by",
+ "TEXT": "Lyricist/Text writer",
+ "TFLT": "File type",
+ "TIME": "Time",
+ "TIT1": "Content group description",
+ "TIT2": "Title/songname/content description",
+ "TIT3": "Subtitle/Description refinement",
+ "TKEY": "Initial key",
+ "TLAN": "Language(s)",
+ "TLEN": "Length",
+ "TMED": "Media type",
+ "TOAL": "Original album/movie/show title",
+ "TOFN": "Original filename",
+ "TOLY": "Original lyricist(s)/text writer(s)",
+ "TOPE": "Original artist(s)/performer(s)",
+ "TORY": "Original release year",
+ "TOWN": "File owner/licensee",
+ "TPE1": "Lead performer(s)/Soloist(s)",
+ "TPE2": "Band/orchestra/accompaniment",
+ "TPE3": "Conductor/performer refinement",
+ "TPE4": "Interpreted, remixed, or otherwise modified by",
+ "TPOS": "Part of a set",
+ "TPUB": "Publisher",
+ "TRCK": "Track number/Position in set",
+ "TRDA": "Recording dates",
+ "TRSN": "Internet radio station name",
+ "TRSO": "Internet radio station owner",
+ "TSIZ": "Size",
+ "TSO2": "iTunes uses this for Album Artist sort order",
+ "TSOC": "iTunes uses this for Composer sort order",
+ "TSRC": "ISRC (international standard recording code)",
+ "TSSE": "Software/Hardware and settings used for encoding",
+ "TYER": "Year",
+ "TXXX": "User defined text information frame",
+ "UFID": "Unique file identifier",
+ "USER": "Terms of use",
+ "USLT": "Unsychronized lyric/text transcription",
+ "WCOM": "Commercial information",
+ "WCOP": "Copyright/Legal information",
+ "WOAF": "Official audio file webpage",
+ "WOAR": "Official artist/performer webpage",
+ "WOAS": "Official audio source webpage",
+ "WORS": "Official internet radio station homepage",
+ "WPAY": "Payment",
+ "WPUB": "Publishers official webpage",
+ "WXXX": "User defined URL link frame",
+}
+
+// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4).
+var id3v24Frames = map[string]string{
+ "AENC": "Audio encryption",
+ "APIC": "Attached picture",
+ "ASPI": "Audio seek point index",
+
+ "COMM": "Comments",
+ "COMR": "Commercial frame",
+
+ "ENCR": "Encryption method registration",
+ "EQU2": "Equalisation (2)",
+ "ETCO": "Event timing codes",
+
+ "GEOB": "General encapsulated object",
+ "GRID": "Group identification registration",
+
+ "LINK": "Linked information",
+
+ "MCDI": "Music CD identifier",
+ "MLLT": "MPEG location lookup table",
+
+ "OWNE": "Ownership frame",
+
+ "PRIV": "Private frame",
+ "PCNT": "Play counter",
+ "POPM": "Popularimeter",
+ "POSS": "Position synchronisation frame",
+
+ "RBUF": "Recommended buffer size",
+ "RVA2": "Relative volume adjustment (2)",
+ "RVRB": "Reverb",
+
+ "SEEK": "Seek frame",
+ "SIGN": "Signature frame",
+ "SYLT": "Synchronised lyric/text",
+ "SYTC": "Synchronised tempo codes",
+
+ "TALB": "Album/Movie/Show title",
+ "TBPM": "BPM (beats per minute)",
+ "TCMP": "iTunes Compilation Flag",
+ "TCOM": "Composer",
+ "TCON": "Content type",
+ "TCOP": "Copyright message",
+ "TDEN": "Encoding time",
+ "TDLY": "Playlist delay",
+ "TDOR": "Original release time",
+ "TDRC": "Recording time",
+ "TDRL": "Release time",
+ "TDTG": "Tagging time",
+ "TENC": "Encoded by",
+ "TEXT": "Lyricist/Text writer",
+ "TFLT": "File type",
+ "TIPL": "Involved people list",
+ "TIT1": "Content group description",
+ "TIT2": "Title/songname/content description",
+ "TIT3": "Subtitle/Description refinement",
+ "TKEY": "Initial key",
+ "TLAN": "Language(s)",
+ "TLEN": "Length",
+ "TMCL": "Musician credits list",
+ "TMED": "Media type",
+ "TMOO": "Mood",
+ "TOAL": "Original album/movie/show title",
+ "TOFN": "Original filename",
+ "TOLY": "Original lyricist(s)/text writer(s)",
+ "TOPE": "Original artist(s)/performer(s)",
+ "TOWN": "File owner/licensee",
+ "TPE1": "Lead performer(s)/Soloist(s)",
+ "TPE2": "Band/orchestra/accompaniment",
+ "TPE3": "Conductor/performer refinement",
+ "TPE4": "Interpreted, remixed, or otherwise modified by",
+ "TPOS": "Part of a set",
+ "TPRO": "Produced notice",
+ "TPUB": "Publisher",
+ "TRCK": "Track number/Position in set",
+ "TRSN": "Internet radio station name",
+ "TRSO": "Internet radio station owner",
+ "TSO2": "iTunes uses this for Album Artist sort order",
+ "TSOA": "Album sort order",
+ "TSOC": "iTunes uses this for Composer sort order",
+ "TSOP": "Performer sort order",
+ "TSOT": "Title sort order",
+ "TSRC": "ISRC (international standard recording code)",
+ "TSSE": "Software/Hardware and settings used for encoding",
+ "TSST": "Set subtitle",
+ "TXXX": "User defined text information frame",
+
+ "UFID": "Unique file identifier",
+ "USER": "Terms of use",
+ "USLT": "Unsynchronised lyric/text transcription",
+
+ "WCOM": "Commercial information",
+ "WCOP": "Copyright/Legal information",
+ "WOAF": "Official audio file webpage",
+ "WOAR": "Official artist/performer webpage",
+ "WOAS": "Official audio source webpage",
+ "WORS": "Official Internet radio station homepage",
+ "WPAY": "Payment",
+ "WPUB": "Publishers official webpage",
+ "WXXX": "User defined URL link frame",
+}
+
+// ID3 frames that are defined in the specs.
+var id3Frames = map[Format]map[string]string{
+ ID3v2_2: id3v22Frames,
+ ID3v2_3: id3v23Frames,
+ ID3v2_4: id3v24Frames,
+}
+
+func validID3Frame(version Format, name string) bool {
+ names, ok := id3Frames[version]
+ if !ok {
+ return false
+ }
+ _, ok = names[name]
+ return ok
+}
+
+func readWFrame(b []byte) (string, error) {
+ // Frame text is always encoded in ISO-8859-1
+ b = append([]byte{0}, b...)
+ return readTFrame(b)
+}
+
+func readTFrame(b []byte) (string, error) {
+ if len(b) == 0 {
+ return "", nil
+ }
+
+ txt, err := decodeText(b[0], b[1:])
+ if err != nil {
+ return "", err
+ }
+ return strings.Join(strings.Split(txt, string(singleZero)), ""), nil
+}
+
+const (
+ encodingISO8859 byte = 0
+ encodingUTF16WithBOM byte = 1
+ encodingUTF16 byte = 2
+ encodingUTF8 byte = 3
+)
+
+func decodeText(enc byte, b []byte) (string, error) {
+ if len(b) == 0 {
+ return "", nil
+ }
+
+ switch enc {
+ case encodingISO8859: // ISO-8859-1
+ return decodeISO8859(b), nil
+
+ case encodingUTF16WithBOM: // UTF-16 with byte order marker
+ if len(b) == 1 {
+ return "", nil
+ }
+ return decodeUTF16WithBOM(b)
+
+ case encodingUTF16: // UTF-16 without byte order (assuming BigEndian)
+ if len(b) == 1 {
+ return "", nil
+ }
+ return decodeUTF16(b, binary.BigEndian)
+
+ case encodingUTF8: // UTF-8
+ return string(b), nil
+
+ default: // Fallback to ISO-8859-1
+ return decodeISO8859(b), nil
+ }
+}
+
+var (
+ singleZero = []byte{0}
+ doubleZero = []byte{0, 0}
+)
+
+func dataSplit(b []byte, enc byte) [][]byte {
+ delim := singleZero
+ if enc == encodingUTF16 || enc == encodingUTF16WithBOM {
+ delim = doubleZero
+ }
+
+ result := bytes.SplitN(b, delim, 2)
+ if len(result) != 2 {
+ return result
+ }
+
+ if len(result[1]) == 0 {
+ return result
+ }
+
+ if result[1][0] == 0 {
+ // there was a double (or triple) 0 and we cut too early
+ result[0] = append(result[0], result[1][0])
+ result[1] = result[1][1:]
+ }
+ return result
+}
+
+func decodeISO8859(b []byte) string {
+ r := make([]rune, len(b))
+ for i, x := range b {
+ r[i] = rune(x)
+ }
+ return string(r)
+}
+
+func decodeUTF16WithBOM(b []byte) (string, error) {
+ if len(b) < 2 {
+ return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark")
+ }
+
+ var bo binary.ByteOrder
+ switch {
+ case b[0] == 0xFE && b[1] == 0xFF:
+ bo = binary.BigEndian
+ b = b[2:]
+
+ case b[0] == 0xFF && b[1] == 0xFE:
+ bo = binary.LittleEndian
+ b = b[2:]
+
+ default:
+ bo = DefaultUTF16WithBOMByteOrder
+ }
+ return decodeUTF16(b, bo)
+}
+
+func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) {
+ if len(b)%2 != 0 {
+ return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text")
+ }
+ s := make([]uint16, 0, len(b)/2)
+ for i := 0; i < len(b); i += 2 {
+ s = append(s, bo.Uint16(b[i:i+2]))
+ }
+ return string(utf16.Decode(s)), nil
+}
+
+// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag.
+// It's a text with a description and a specified language
+// For WXXX, TXXX and UFID, we don't set a Language
+type Comm struct {
+ Language string
+ Description string
+ Text string
+}
+
+// String returns a string representation of the underlying Comm instance.
+func (t Comm) String() string {
+ if t.Language != "" {
+ return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}",
+ t.Language, t.Description, strings.Count(t.Text, "\n"))
+ }
+ return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text)
+}
+
+// IDv2.{3,4}
+// -- Header
+//
+//
+// -- readTextWithDescrFrame(data, true, true)
+// Text encoding $xx
+// Language $xx xx xx
+// Content descriptor $00 (00)
+// Lyrics/text
+// -- Header
+//
+//
+// -- readTextWithDescrFrame(data, false, )
+// Text encoding $xx
+// Description $00 (00)
+// Value
+func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) {
+ if len(b) == 0 {
+ return nil, errors.New("error decoding tag description text: invalid encoding")
+ }
+ enc := b[0]
+ b = b[1:]
+
+ c := &Comm{}
+ if hasLang {
+ if len(b) < 3 {
+ return nil, errors.New("hasLang set but not enough data for language information")
+ }
+ c.Language = string(b[:3])
+ b = b[3:]
+ }
+
+ descTextSplit := dataSplit(b, enc)
+ if len(descTextSplit) == 0 {
+ return nil, errors.New("error decoding tag description text: invalid encoding")
+ }
+
+ desc, err := decodeText(enc, descTextSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding tag description text: %v", err)
+ }
+ c.Description = desc
+
+ if len(descTextSplit) == 1 {
+ return c, nil
+ }
+
+ if !encoded {
+ enc = byte(0)
+ }
+ text, err := decodeText(enc, descTextSplit[1])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding tag text: %v", err)
+ }
+ c.Text = text
+
+ return c, nil
+}
+
+// UFID is composed of a provider (frequently a URL and a binary identifier)
+// The identifier can be a text (Musicbrainz use texts, but not necessary)
+type UFID struct {
+ Provider string
+ Identifier []byte
+}
+
+func (u UFID) String() string {
+ return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier))
+}
+
+func readUFID(b []byte) (*UFID, error) {
+ result := bytes.SplitN(b, singleZero, 2)
+ if len(result) != 2 {
+ return nil, errors.New("expected to split UFID data into 2 pieces")
+ }
+
+ return &UFID{
+ Provider: string(result[0]),
+ Identifier: result[1],
+ }, nil
+}
+
+var pictureTypes = map[byte]string{
+ 0x00: "Other",
+ 0x01: "32x32 pixels 'file icon' (PNG only)",
+ 0x02: "Other file icon",
+ 0x03: "Cover (front)",
+ 0x04: "Cover (back)",
+ 0x05: "Leaflet page",
+ 0x06: "Media (e.g. lable side of CD)",
+ 0x07: "Lead artist/lead performer/soloist",
+ 0x08: "Artist/performer",
+ 0x09: "Conductor",
+ 0x0A: "Band/Orchestra",
+ 0x0B: "Composer",
+ 0x0C: "Lyricist/text writer",
+ 0x0D: "Recording Location",
+ 0x0E: "During recording",
+ 0x0F: "During performance",
+ 0x10: "Movie/video screen capture",
+ 0x11: "A bright coloured fish",
+ 0x12: "Illustration",
+ 0x13: "Band/artist logotype",
+ 0x14: "Publisher/Studio logotype",
+}
+
+// Picture is a type which represents an attached picture extracted from metadata.
+type Picture struct {
+ Ext string // Extension of the picture file.
+ MIMEType string // MIMEType of the picture.
+ Type string // Type of the picture (see pictureTypes).
+ Description string // Description.
+ Data []byte // Raw picture data.
+}
+
+// String returns a string representation of the underlying Picture instance.
+func (p Picture) String() string {
+ return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}",
+ p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data))
+}
+
+// IDv2.2
+// -- Header
+// Attached picture "PIC"
+// Frame size $xx xx xx
+// -- readPICFrame
+// Text encoding $xx
+// Image format $xx xx xx
+// Picture type $xx
+// Description $00 (00)
+// Picture data
+func readPICFrame(b []byte) (*Picture, error) {
+ if len(b) < 5 {
+ return nil, errors.New("invalid PIC frame")
+ }
+
+ enc := b[0]
+ ext := string(b[1:4])
+ picType := b[4]
+
+ descDataSplit := dataSplit(b[5:], enc)
+ if len(descDataSplit) != 2 {
+ return nil, errors.New("error decoding PIC description text: invalid encoding")
+ }
+ desc, err := decodeText(enc, descDataSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding PIC description text: %v", err)
+ }
+
+ var mimeType string
+ switch ext {
+ case "jpeg", "jpg":
+ mimeType = "image/jpeg"
+ case "png":
+ mimeType = "image/png"
+ }
+
+ return &Picture{
+ Ext: ext,
+ MIMEType: mimeType,
+ Type: pictureTypes[picType],
+ Description: desc,
+ Data: descDataSplit[1],
+ }, nil
+}
+
+// IDv2.{3,4}
+// -- Header
+//
+// -- readAPICFrame
+// Text encoding $xx
+// MIME type $00
+// Picture type $xx
+// Description $00 (00)
+// Picture data
+func readAPICFrame(b []byte) (*Picture, error) {
+ if len(b) == 0 {
+ return nil, errors.New("error decoding APIC: invalid encoding")
+ }
+ enc := b[0]
+ mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2)
+ if len(mimeDataSplit) != 2 {
+ return nil, errors.New("error decoding APIC: invalid encoding")
+ }
+
+ mimeType := string(mimeDataSplit[0])
+
+ b = mimeDataSplit[1]
+ if len(b) < 1 {
+ return nil, fmt.Errorf("error decoding APIC mimetype")
+ }
+ picType := b[0]
+
+ descDataSplit := dataSplit(b[1:], enc)
+ if len(descDataSplit) != 2 {
+ return nil, errors.New("error decoding APIC description text: invalid encoding")
+ }
+ desc, err := decodeText(enc, descDataSplit[0])
+ if err != nil {
+ return nil, fmt.Errorf("error decoding APIC description text: %v", err)
+ }
+
+ var ext string
+ switch mimeType {
+ case "image/jpeg":
+ ext = "jpg"
+ case "image/png":
+ ext = "png"
+ }
+
+ return &Picture{
+ Ext: ext,
+ MIMEType: mimeType,
+ Type: pictureTypes[picType],
+ Description: desc,
+ Data: descDataSplit[1],
+ }, nil
+}
diff --git a/vendor/github.com/dhowden/tag/id3v2metadata.go b/vendor/github.com/dhowden/tag/id3v2metadata.go
new file mode 100644
index 0000000..4279f64
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/id3v2metadata.go
@@ -0,0 +1,152 @@
+// 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"
+ "time"
+)
+
+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 {
+ stringYear := m.getString(frames.Name("year", m.Format()))
+
+ if year, err := strconv.Atoi(stringYear); err == nil {
+ return year
+ }
+
+ date, err := time.Parse(time.DateOnly, stringYear)
+ if err != nil {
+ return 0
+ }
+
+ return date.Year()
+}
+
+func parseXofN(s string) (x, n int) {
+ xn := strings.Split(s, "/")
+ if len(xn) != 2 {
+ x, _ = strconv.Atoi(s)
+ return x, 0
+ }
+ x, _ = strconv.Atoi(strings.TrimSpace(xn[0]))
+ n, _ = strconv.Atoi(strings.TrimSpace(xn[1]))
+ return x, n
+}
+
+func (m metadataID3v2) Track() (int, int) {
+ return parseXofN(m.getString(frames.Name("track", m.Format())))
+}
+
+func (m metadataID3v2) Disc() (int, int) {
+ return parseXofN(m.getString(frames.Name("disc", m.Format())))
+}
+
+func (m metadataID3v2) Lyrics() string {
+ t, ok := m.frames[frames.Name("lyrics", m.Format())]
+ if !ok {
+ return ""
+ }
+ return t.(*Comm).Text
+}
+
+func (m metadataID3v2) Comment() string {
+ t, ok := m.frames[frames.Name("comment", m.Format())]
+ if !ok {
+ return ""
+ }
+ // id3v23 has Text, id3v24 has Description
+ if t.(*Comm).Description == "" {
+ return trimString(t.(*Comm).Text)
+ }
+ return trimString(t.(*Comm).Description)
+}
+
+func (m metadataID3v2) Picture() *Picture {
+ v, ok := m.frames[frames.Name("picture", m.Format())]
+ if !ok {
+ return nil
+ }
+ return v.(*Picture)
+}
diff --git a/vendor/github.com/dhowden/tag/mp4.go b/vendor/github.com/dhowden/tag/mp4.go
new file mode 100644
index 0000000..19c1a81
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/mp4.go
@@ -0,0 +1,379 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tag
+
+import (
+ "bytes"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "io"
+ "strconv"
+ "strings"
+)
+
+var atomTypes = map[int]string{
+ 0: "implicit", // automatic based on atom name
+ 1: "text",
+ 13: "jpeg",
+ 14: "png",
+ 21: "uint8",
+}
+
+// NB: atoms does not include "----", this is handled separately
+var atoms = atomNames(map[string]string{
+ "\xa9alb": "album",
+ "\xa9art": "artist",
+ "\xa9ART": "artist",
+ "aART": "album_artist",
+ "\xa9day": "year",
+ "\xa9nam": "title",
+ "\xa9gen": "genre",
+ "trkn": "track",
+ "\xa9wrt": "composer",
+ "\xa9too": "encoder",
+ "cprt": "copyright",
+ "covr": "picture",
+ "\xa9grp": "grouping",
+ "keyw": "keyword",
+ "\xa9lyr": "lyrics",
+ "\xa9cmt": "comment",
+ "tmpo": "tempo",
+ "cpil": "compilation",
+ "disk": "disc",
+})
+
+var means = map[string]bool{
+ "com.apple.iTunes": true,
+ "com.mixedinkey.mixedinkey": true,
+ "com.serato.dj": true,
+}
+
+// Detect PNG image if "implicit" class is used
+var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10}
+
+type atomNames map[string]string
+
+func (f atomNames) Name(n string) []string {
+ res := make([]string, 1)
+ for k, v := range f {
+ if v == n {
+ res = append(res, k)
+ }
+ }
+ return res
+}
+
+// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data.
+type metadataMP4 struct {
+ fileType FileType
+ data map[string]interface{}
+}
+
+// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning
+// non-nil error if there was a problem.
+func ReadAtoms(r io.ReadSeeker) (Metadata, error) {
+ m := metadataMP4{
+ data: make(map[string]interface{}),
+ fileType: UnknownFileType,
+ }
+ err := m.readAtoms(r)
+ return m, err
+}
+
+func (m metadataMP4) readAtoms(r io.ReadSeeker) error {
+ for {
+ name, size, err := readAtomHeader(r)
+ if err != nil {
+ if err == io.EOF {
+ return nil
+ }
+ return err
+ }
+
+ switch name {
+ case "meta":
+ // next_item_id (int32)
+ _, err := readBytes(r, 4)
+ if err != nil {
+ return err
+ }
+ fallthrough
+
+ case "moov", "udta", "ilst":
+ return m.readAtoms(r)
+ }
+
+ _, ok := atoms[name]
+ var data []string
+ if name == "----" {
+ name, data, err = readCustomAtom(r, size)
+ if err != nil {
+ return err
+ }
+
+ if name != "----" {
+ ok = true
+ size = 0 // already read data
+ }
+ }
+
+ if !ok {
+ _, err := r.Seek(int64(size-8), io.SeekCurrent)
+ if err != nil {
+ return err
+ }
+ continue
+ }
+
+ err = m.readAtomData(r, name, size-8, data)
+ if err != nil {
+ return err
+ }
+ }
+}
+
+func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error {
+ var b []byte
+ var err error
+ var contentType string
+ if len(processedData) > 0 {
+ b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields
+ contentType = "text"
+ } else {
+ // read the data
+ b, err = readBytes(r, uint(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) < 4 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 4, len(b))
+ }
+ class := getInt(b[1:4])
+ var ok bool
+ contentType, ok = atomTypes[class]
+ if !ok {
+ return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b)
+ }
+
+ // 4: atom version (1 byte) + atom flags (3 bytes)
+ // 4: NULL (usually locale indicator)
+ if len(b) < 8 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b))
+ }
+ b = b[8:]
+ }
+
+ if name == "trkn" || name == "disk" {
+ if len(b) < 6 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b))
+ }
+
+ m.data[name] = int(b[3])
+ m.data[name+"_count"] = int(b[5])
+ return nil
+ }
+
+ if contentType == "implicit" {
+ if name == "covr" {
+ if bytes.HasPrefix(b, pngHeader) {
+ contentType = "png"
+ }
+ // TODO(dhowden): Detect JPEG formats too (harder).
+ }
+ }
+
+ var data interface{}
+ switch contentType {
+ case "implicit":
+ if _, ok := atoms[name]; ok {
+ return fmt.Errorf("unhandled implicit content type for required atom: %q", name)
+ }
+ return nil
+
+ case "text":
+ data = string(b)
+
+ case "uint8":
+ if len(b) < 1 {
+ return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b))
+ }
+ data = getInt(b[:1])
+
+ case "jpeg", "png":
+ data = &Picture{
+ Ext: contentType,
+ MIMEType: "image/" + contentType,
+ Data: b,
+ }
+ }
+ m.data[name] = data
+
+ return nil
+}
+
+func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) {
+ err = binary.Read(r, binary.BigEndian, &size)
+ if err != nil {
+ return
+ }
+ name, err = readString(r, 4)
+ return
+}
+
+// Generic atom.
+// Should have 3 sub atoms : mean, name and data.
+// We check that mean is "com.apple.iTunes" or others and we use the subname as
+// the name, and move to the data atom.
+// Data atom could have multiple data values, each with a header.
+// If anything goes wrong, we jump at the end of the "----" atom.
+func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) {
+ subNames := make(map[string]string)
+
+ for size > 8 {
+ subName, subSize, err := readAtomHeader(r)
+ if err != nil {
+ return "", nil, err
+ }
+
+ // Remove the size of the atom from the size counter
+ if size >= subSize {
+ size -= subSize
+ } else {
+ return "", nil, errors.New("--- invalid size")
+ }
+
+ b, err := readBytes(r, uint(subSize-8))
+ if err != nil {
+ return "", nil, err
+ }
+
+ if len(b) < 4 {
+ return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b))
+ }
+ switch subName {
+ case "mean", "name":
+ subNames[subName] = string(b[4:])
+ case "data":
+ data = append(data, string(b[4:]))
+ }
+ }
+
+ // there should remain only the header size
+ if size != 8 {
+ err := errors.New("---- atom out of bounds")
+ return "", nil, err
+ }
+
+ if !means[subNames["mean"]] || subNames["name"] == "" || len(data) == 0 {
+ return "----", nil, nil
+ }
+
+ return subNames["name"], data, nil
+}
+
+func (metadataMP4) Format() Format { return MP4 }
+func (m metadataMP4) FileType() FileType { return m.fileType }
+
+func (m metadataMP4) Raw() map[string]interface{} { return m.data }
+
+func (m metadataMP4) getString(n []string) string {
+ for _, k := range n {
+ if x, ok := m.data[k]; ok {
+ return x.(string)
+ }
+ }
+ return ""
+}
+
+func (m metadataMP4) getInt(n []string) int {
+ for _, k := range n {
+ if x, ok := m.data[k]; ok {
+ return x.(int)
+ }
+ }
+ return 0
+}
+
+func (m metadataMP4) Title() string {
+ return m.getString(atoms.Name("title"))
+}
+
+func (m metadataMP4) Artist() string {
+ return m.getString(atoms.Name("artist"))
+}
+
+func (m metadataMP4) Album() string {
+ return m.getString(atoms.Name("album"))
+}
+
+func (m metadataMP4) AlbumArtist() string {
+ return m.getString(atoms.Name("album_artist"))
+}
+
+func (m metadataMP4) Composer() string {
+ return m.getString(atoms.Name("composer"))
+}
+
+func (m metadataMP4) Genre() string {
+ return m.getString(atoms.Name("genre"))
+}
+
+func (m metadataMP4) Year() int {
+ date := m.getString(atoms.Name("year"))
+ if len(date) >= 4 {
+ year, _ := strconv.Atoi(date[:4])
+ return year
+ }
+ return 0
+}
+
+func (m metadataMP4) Track() (int, int) {
+ x := m.getInt([]string{"trkn"})
+ if n, ok := m.data["trkn_count"]; ok {
+ return x, n.(int)
+ }
+ return x, 0
+}
+
+func (m metadataMP4) Disc() (int, int) {
+ x := m.getInt([]string{"disk"})
+ if n, ok := m.data["disk_count"]; ok {
+ return x, n.(int)
+ }
+ return x, 0
+}
+
+func (m metadataMP4) Lyrics() string {
+ t, ok := m.data["\xa9lyr"]
+ if !ok {
+ return ""
+ }
+ return t.(string)
+}
+
+func (m metadataMP4) Comment() string {
+ t, ok := m.data["\xa9cmt"]
+ if !ok {
+ return ""
+ }
+ return t.(string)
+}
+
+func (m metadataMP4) Picture() *Picture {
+ v, ok := m.data["covr"]
+ if !ok {
+ return nil
+ }
+ p, _ := v.(*Picture)
+ return p
+}
diff --git a/vendor/github.com/dhowden/tag/ogg.go b/vendor/github.com/dhowden/tag/ogg.go
new file mode 100644
index 0000000..f5c4770
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/ogg.go
@@ -0,0 +1,174 @@
+// 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"
+)
+
+var (
+ vorbisCommentPrefix = []byte("\x03vorbis")
+ opusTagsPrefix = []byte("OpusTags")
+)
+
+var oggCRC32Poly04c11db7 = oggCRCTable(0x04c11db7)
+
+type crc32Table [256]uint32
+
+func oggCRCTable(poly uint32) *crc32Table {
+ var t crc32Table
+
+ for i := 0; i < 256; i++ {
+ crc := uint32(i) << 24
+ for j := 0; j < 8; j++ {
+ if crc&0x80000000 != 0 {
+ crc = (crc << 1) ^ poly
+ } else {
+ crc <<= 1
+ }
+ }
+ t[i] = crc
+ }
+
+ return &t
+}
+
+func oggCRCUpdate(crc uint32, tab *crc32Table, p []byte) uint32 {
+ for _, v := range p {
+ crc = (crc << 8) ^ tab[byte(crc>>24)^v]
+ }
+ return crc
+}
+
+type oggPageHeader struct {
+ Magic [4]byte // "OggS"
+ Version uint8
+ Flags uint8
+ GranulePosition uint64
+ SerialNumber uint32
+ SequenceNumber uint32
+ CRC uint32
+ Segments uint8
+}
+
+type oggDemuxer struct {
+ packetBufs map[uint32]*bytes.Buffer
+}
+
+// Read ogg packets, can return empty slice of packets and nil err
+// if more data is needed
+func (o *oggDemuxer) Read(r io.Reader) ([][]byte, error) {
+ headerBuf := &bytes.Buffer{}
+ var oh oggPageHeader
+ if err := binary.Read(io.TeeReader(r, headerBuf), binary.LittleEndian, &oh); err != nil {
+ return nil, err
+ }
+
+ if bytes.Compare(oh.Magic[:], []byte("OggS")) != 0 {
+ // TODO: seek for syncword?
+ return nil, errors.New("expected 'OggS'")
+ }
+
+ segmentTable := make([]byte, oh.Segments)
+ if _, err := io.ReadFull(r, segmentTable); err != nil {
+ return nil, err
+ }
+ var segmentsSize int64
+ for _, s := range segmentTable {
+ segmentsSize += int64(s)
+ }
+ segmentsData := make([]byte, segmentsSize)
+ if _, err := io.ReadFull(r, segmentsData); err != nil {
+ return nil, err
+ }
+
+ headerBytes := headerBuf.Bytes()
+ // reset CRC to zero in header before checksum
+ headerBytes[22] = 0
+ headerBytes[23] = 0
+ headerBytes[24] = 0
+ headerBytes[25] = 0
+ crc := oggCRCUpdate(0, oggCRC32Poly04c11db7, headerBytes)
+ crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentTable)
+ crc = oggCRCUpdate(crc, oggCRC32Poly04c11db7, segmentsData)
+ if crc != oh.CRC {
+ return nil, fmt.Errorf("expected crc %x != %x", oh.CRC, crc)
+ }
+
+ if o.packetBufs == nil {
+ o.packetBufs = map[uint32]*bytes.Buffer{}
+ }
+
+ var packetBuf *bytes.Buffer
+ continued := oh.Flags&0x1 != 0
+ if continued {
+ if b, ok := o.packetBufs[oh.SerialNumber]; ok {
+ packetBuf = b
+ } else {
+ return nil, fmt.Errorf("could not find continued packet %d", oh.SerialNumber)
+ }
+ } else {
+ packetBuf = &bytes.Buffer{}
+ }
+
+ var packets [][]byte
+ var p int
+ for _, s := range segmentTable {
+ packetBuf.Write(segmentsData[p : p+int(s)])
+ if s < 255 {
+ packets = append(packets, packetBuf.Bytes())
+ packetBuf = &bytes.Buffer{}
+ }
+ p += int(s)
+ }
+
+ o.packetBufs[oh.SerialNumber] = packetBuf
+
+ return packets, nil
+}
+
+// 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.
+// For Opus see https://tools.ietf.org/html/rfc7845
+func ReadOGGTags(r io.Reader) (Metadata, error) {
+ od := &oggDemuxer{}
+ for {
+ bs, err := od.Read(r)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, b := range bs {
+ switch {
+ case bytes.HasPrefix(b, vorbisCommentPrefix):
+ m := &metadataOGG{
+ newMetadataVorbis(),
+ }
+ err = m.readVorbisComment(bytes.NewReader(b[len(vorbisCommentPrefix):]))
+ return m, err
+ case bytes.HasPrefix(b, opusTagsPrefix):
+ m := &metadataOGG{
+ newMetadataVorbis(),
+ }
+ err = m.readVorbisComment(bytes.NewReader(b[len(opusTagsPrefix):]))
+ return m, err
+ }
+ }
+ }
+}
+
+type metadataOGG struct {
+ *metadataVorbis
+}
+
+func (m *metadataOGG) FileType() FileType {
+ return OGG
+}
diff --git a/vendor/github.com/dhowden/tag/sum.go b/vendor/github.com/dhowden/tag/sum.go
new file mode 100644
index 0000000..97b27a7
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/sum.go
@@ -0,0 +1,219 @@
+package tag
+
+import (
+ "crypto/sha1"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "hash"
+ "io"
+)
+
+// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata
+// (ID3, MP4) invariant.
+func Sum(r io.ReadSeeker) (string, error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return "", err
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("could not seek back to original position: %v", err)
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return SumFLAC(r)
+
+ case string(b[4:11]) == "ftypM4A":
+ return SumAtoms(r)
+
+ case string(b[0:3]) == "ID3":
+ return SumID3v2(r)
+ }
+
+ h, err := SumID3v1(r)
+ if err != nil {
+ if err == ErrNotID3v1 {
+ return SumAll(r)
+ }
+ return "", err
+ }
+ return h, nil
+}
+
+// SumAll returns a checksum of the content from the reader (until EOF).
+func SumAll(r io.ReadSeeker) (string, error) {
+ h := sha1.New()
+ _, err := io.Copy(h, r)
+ if err != nil {
+ return "", nil
+ }
+ return hashSum(h), nil
+}
+
+// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is
+// metadata invariant.
+func SumAtoms(r io.ReadSeeker) (string, error) {
+ for {
+ var size uint32
+ err := binary.Read(r, binary.BigEndian, &size)
+ if err != nil {
+ if err == io.EOF {
+ return "", fmt.Errorf("reached EOF before audio data")
+ }
+ return "", err
+ }
+
+ name, err := readString(r, 4)
+ if err != nil {
+ return "", err
+ }
+
+ switch name {
+ case "meta":
+ // next_item_id (int32)
+ _, err := r.Seek(4, io.SeekCurrent)
+ if err != nil {
+ return "", err
+ }
+ fallthrough
+
+ case "moov", "udta", "ilst":
+ continue
+
+ case "mdat": // stop when we get to the data
+ h := sha1.New()
+ _, err := io.CopyN(h, r, int64(size-8))
+ if err != nil {
+ return "", fmt.Errorf("error reading audio data: %v", err)
+ }
+ return hashSum(h), nil
+ }
+
+ _, err = r.Seek(int64(size-8), io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("error reading '%v' tag: %v", name, err)
+ }
+ }
+}
+
+func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) {
+ n, err := r.Seek(-128, io.SeekEnd)
+ if err != nil {
+ return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err)
+ }
+
+ _, err = r.Seek(-n, io.SeekCurrent)
+ if err != nil {
+ return 0, fmt.Errorf("error seeking back to original position: %v", err)
+ }
+ return n, nil
+}
+
+// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided
+// by the io.ReadSeeker which is metadata invariant.
+func SumID3v1(r io.ReadSeeker) (string, error) {
+ n, err := sizeToEndOffset(r, 128)
+ if err != nil {
+ return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+ }
+
+ // TODO: improve this check???
+ if n <= 0 {
+ return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3")
+ }
+
+ h := sha1.New()
+ _, err = io.CopyN(h, r, n)
+ if err != nil {
+ return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+ }
+ return hashSum(h), nil
+}
+
+// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the
+// io.ReadSeeker which is metadata invariant.
+func SumID3v2(r io.ReadSeeker) (string, error) {
+ header, _, err := readID3v2Header(r)
+ if err != nil {
+ return "", fmt.Errorf("error reading ID3v2 header: %v", err)
+ }
+
+ _, err = r.Seek(int64(header.Size), io.SeekCurrent)
+ if err != nil {
+ return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err)
+ }
+
+ n, err := sizeToEndOffset(r, 128)
+ if err != nil {
+ return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err)
+ }
+
+ // TODO: remove this check?????
+ if n < 0 {
+ return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n)
+ }
+
+ h := sha1.New()
+ _, err = io.CopyN(h, r, n)
+ if err != nil {
+ return "", fmt.Errorf("error reading %v bytes: %v", n, err)
+ }
+ return hashSum(h), nil
+}
+
+// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores
+// metadata fields).
+func SumFLAC(r io.ReadSeeker) (string, error) {
+ flac, err := readString(r, 4)
+ if err != nil {
+ return "", err
+ }
+ if flac != "fLaC" {
+ return "", errors.New("expected 'fLaC'")
+ }
+
+ for {
+ last, err := skipFLACMetadataBlock(r)
+ if err != nil {
+ return "", err
+ }
+
+ if last {
+ break
+ }
+ }
+
+ h := sha1.New()
+ _, err = io.Copy(h, r)
+ if err != nil {
+ return "", fmt.Errorf("error reading data bytes from FLAC: %v", err)
+ }
+ return hashSum(h), nil
+}
+
+func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) {
+ blockHeader, err := readBytes(r, 1)
+ if err != nil {
+ return
+ }
+
+ if getBit(blockHeader[0], 7) {
+ blockHeader[0] ^= (1 << 7)
+ last = true
+ }
+
+ blockLen, err := readInt(r, 3)
+ if err != nil {
+ return
+ }
+
+ _, err = r.Seek(int64(blockLen), io.SeekCurrent)
+ return
+}
+
+func hashSum(h hash.Hash) string {
+ return fmt.Sprintf("%x", h.Sum([]byte{}))
+}
diff --git a/vendor/github.com/dhowden/tag/tag.go b/vendor/github.com/dhowden/tag/tag.go
new file mode 100644
index 0000000..306f1d7
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/tag.go
@@ -0,0 +1,147 @@
+// Copyright 2015, David Howden
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package tag provides MP3 (ID3: v1, 2.2, 2.3 and 2.4), MP4, FLAC and OGG metadata detection,
+// parsing and artwork extraction.
+//
+// Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File):
+// m, err := tag.ReadFrom(f)
+// if err != nil {
+// log.Fatal(err)
+// }
+// log.Print(m.Format()) // The detected format.
+// log.Print(m.Title()) // The title of the track (see Metadata interface for more details).
+package tag
+
+import (
+ "errors"
+ "fmt"
+ "io"
+)
+
+// ErrNoTagsFound is the error returned by ReadFrom when the metadata format
+// cannot be identified.
+var ErrNoTagsFound = errors.New("no tags found")
+
+// ReadFrom detects and parses audio file metadata tags (currently supports ID3v1,2.{2,3,4}, MP4, FLAC/OGG).
+// Returns non-nil error if the format of the given data could not be determined, or if there was a problem
+// parsing the data.
+func ReadFrom(r io.ReadSeeker) (Metadata, error) {
+ b, err := readBytes(r, 11)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = r.Seek(-11, io.SeekCurrent)
+ if err != nil {
+ return nil, fmt.Errorf("could not seek back to original position: %v", err)
+ }
+
+ switch {
+ case string(b[0:4]) == "fLaC":
+ return ReadFLACTags(r)
+
+ case string(b[0:4]) == "OggS":
+ return ReadOGGTags(r)
+
+ case string(b[4:8]) == "ftyp":
+ return ReadAtoms(r)
+
+ case string(b[0:3]) == "ID3":
+ return ReadID3v2Tags(r)
+
+ case string(b[0:4]) == "DSD ":
+ return ReadDSFTags(r)
+ }
+
+ m, err := ReadID3v1Tags(r)
+ if err != nil {
+ if err == ErrNotID3v1 {
+ err = ErrNoTagsFound
+ }
+ return nil, err
+ }
+ return m, nil
+}
+
+// Format is an enumeration of metadata types supported by this package.
+type Format string
+
+// Supported tag formats.
+const (
+ UnknownFormat Format = "" // Unknown Format.
+ ID3v1 Format = "ID3v1" // ID3v1 tag format.
+ ID3v2_2 Format = "ID3v2.2" // ID3v2.2 tag format.
+ ID3v2_3 Format = "ID3v2.3" // ID3v2.3 tag format (most common).
+ ID3v2_4 Format = "ID3v2.4" // ID3v2.4 tag format.
+ MP4 Format = "MP4" // MP4 tag (atom) format (see http://www.ftyps.com/ for a full file type list)
+ VORBIS Format = "VORBIS" // Vorbis Comment tag format.
+)
+
+// FileType is an enumeration of the audio file types supported by this package, in particular
+// there are audio file types which share metadata formats, and this type is used to distinguish
+// between them.
+type FileType string
+
+// Supported file types.
+const (
+ UnknownFileType FileType = "" // Unknown FileType.
+ MP3 FileType = "MP3" // MP3 file
+ M4A FileType = "M4A" // M4A file Apple iTunes (ACC) Audio
+ M4B FileType = "M4B" // M4A file Apple iTunes (ACC) Audio Book
+ M4P FileType = "M4P" // M4A file Apple iTunes (ACC) AES Protected Audio
+ ALAC FileType = "ALAC" // Apple Lossless file FIXME: actually detect this
+ FLAC FileType = "FLAC" // FLAC file
+ OGG FileType = "OGG" // OGG file
+ DSF FileType = "DSF" // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf
+)
+
+// Metadata is an interface which is used to describe metadata retrieved by this package.
+type Metadata interface {
+ // Format returns the metadata Format used to encode the data.
+ Format() Format
+
+ // FileType returns the file type of the audio file.
+ FileType() FileType
+
+ // Title returns the title of the track.
+ Title() string
+
+ // Album returns the album name of the track.
+ Album() string
+
+ // Artist returns the artist name of the track.
+ Artist() string
+
+ // AlbumArtist returns the album artist name of the track.
+ AlbumArtist() string
+
+ // Composer returns the composer of the track.
+ Composer() string
+
+ // Year returns the year of the track.
+ Year() int
+
+ // Genre returns the genre of the track.
+ Genre() string
+
+ // Track returns the track number and total tracks, or zero values if unavailable.
+ Track() (int, int)
+
+ // Disc returns the disc number and total discs, or zero values if unavailable.
+ Disc() (int, int)
+
+ // Picture returns a picture, or nil if not available.
+ Picture() *Picture
+
+ // Lyrics returns the lyrics, or an empty string if unavailable.
+ Lyrics() string
+
+ // Comment returns the comment, or an empty string if unavailable.
+ Comment() string
+
+ // Raw returns the raw mapping of retrieved tag names and associated values.
+ // NB: tag/atom names are not standardised between formats.
+ Raw() map[string]interface{}
+}
diff --git a/vendor/github.com/dhowden/tag/util.go b/vendor/github.com/dhowden/tag/util.go
new file mode 100644
index 0000000..c738d2f
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/util.go
@@ -0,0 +1,102 @@
+// 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"
+ "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 readUint64LittleEndian(r io.Reader) (uint64, error) {
+ b, err := readBytes(r, 8)
+ if err != nil {
+ return 0, err
+ }
+ return binary.LittleEndian.Uint64(b), nil
+}
+
+// readBytesMaxUpfront is the max up-front allocation allowed
+const readBytesMaxUpfront = 10 << 20 // 10MB
+
+func readBytes(r io.Reader, n uint) ([]byte, error) {
+ if n > readBytesMaxUpfront {
+ b := &bytes.Buffer{}
+ if _, err := io.CopyN(b, r, int64(n)); err != nil {
+ return nil, err
+ }
+ return b.Bytes(), nil
+ }
+
+ b := make([]byte, n)
+ _, err := io.ReadFull(r, b)
+ if err != nil {
+ return nil, err
+ }
+ return b, nil
+}
+
+func readString(r io.Reader, n uint) (string, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+}
+
+func readUint(r io.Reader, n uint) (uint, error) {
+ x, err := readInt(r, n)
+ if err != nil {
+ return 0, err
+ }
+ return uint(x), nil
+}
+
+func readInt(r io.Reader, n uint) (int, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return 0, err
+ }
+ return getInt(b), nil
+}
+
+func read7BitChunkedUint(r io.Reader, n uint) (uint, error) {
+ b, err := readBytes(r, n)
+ if err != nil {
+ return 0, err
+ }
+ return uint(get7BitChunkedInt(b)), nil
+}
+
+func readUint32LittleEndian(r io.Reader) (uint32, error) {
+ b, err := readBytes(r, 4)
+ if err != nil {
+ return 0, err
+ }
+ return binary.LittleEndian.Uint32(b), nil
+}
diff --git a/vendor/github.com/dhowden/tag/vorbis.go b/vendor/github.com/dhowden/tag/vorbis.go
new file mode 100644
index 0000000..f1c6a19
--- /dev/null
+++ b/vendor/github.com/dhowden/tag/vorbis.go
@@ -0,0 +1,266 @@
+// 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/base64"
+ "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 := readUint32LittleEndian(r)
+ if err != nil {
+ return err
+ }
+
+ vendor, err := readString(r, uint(vendorLen))
+ if err != nil {
+ return err
+ }
+ m.c["vendor"] = vendor
+
+ commentsLen, err := readUint32LittleEndian(r)
+ if err != nil {
+ return err
+ }
+
+ for i := uint32(0); i < commentsLen; i++ {
+ l, err := readUint32LittleEndian(r)
+ if err != nil {
+ return err
+ }
+ s, err := readString(r, uint(l))
+ if err != nil {
+ return err
+ }
+ k, v, err := parseComment(s)
+ if err != nil {
+ return err
+ }
+ m.c[strings.ToLower(k)] = v
+ }
+
+ if b64data, ok := m.c["metadata_block_picture"]; ok {
+ data, err := base64.StdEncoding.DecodeString(b64data)
+ if err != nil {
+ return err
+ }
+ m.readPictureBlock(bytes.NewReader(data))
+ }
+
+ 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 := readUint(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 := readUint(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 {
+ // 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.
+ 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 {
+ if m.c["composer"] != "" {
+ return m.c["composer"]
+ }
+ // 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) 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:
+ // Fallback on year tag as some files use that.
+ if len(m.c["year"]) != 0 {
+ year, err := strconv.Atoi(m.c["year"])
+ if err == nil {
+ return year
+ }
+ }
+ return 0
+ case 4:
+ dateFormat = "2006"
+ case 7:
+ dateFormat = "2006-01"
+ case 10:
+ dateFormat = "2006-01-02"
+ }
+
+ t, _ := time.Parse(dateFormat, m.c["date"])
+ return t.Year()
+}
+
+func (m *metadataVorbis) Track() (int, int) {
+ x, _ := strconv.Atoi(m.c["tracknumber"])
+ // https://wiki.xiph.org/Field_names
+ n, _ := strconv.Atoi(m.c["tracktotal"])
+ return x, n
+}
+
+func (m *metadataVorbis) Disc() (int, int) {
+ // https://wiki.xiph.org/Field_names
+ x, _ := strconv.Atoi(m.c["discnumber"])
+ n, _ := strconv.Atoi(m.c["disctotal"])
+ return x, n
+}
+
+func (m *metadataVorbis) Lyrics() string {
+ return m.c["lyrics"]
+}
+
+func (m *metadataVorbis) Comment() string {
+ if m.c["comment"] != "" {
+ return m.c["comment"]
+ }
+ return m.c["description"]
+}
+
+func (m *metadataVorbis) Picture() *Picture {
+ return m.p
+}
diff --git a/vendor/modules.txt b/vendor/modules.txt
new file mode 100644
index 0000000..67fa028
--- /dev/null
+++ b/vendor/modules.txt
@@ -0,0 +1,3 @@
+# github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
+## explicit; go 1.20
+github.com/dhowden/tag