From e1090a7583cfbb75fa03eb56d2b26d68732282cf Mon Sep 17 00:00:00 2001 From: davy Date: Wed, 26 Jun 2019 14:02:31 -0500 Subject: [PATCH] import code --- .gitignore | 2 + Gopkg.lock | 21 + Gopkg.toml | 38 + main.go | 97 +++ pkg/media/library.go | 56 ++ pkg/media/playlist.go | 15 + pkg/media/video.go | 40 + static/css/index.css | 166 ++++ static/defaulticon.jpg | Bin 0 -> 34694 bytes static/favicon.ico | Bin 0 -> 32038 bytes templates/index.html | 36 + vendor/github.com/dhowden/tag/.editorconfig | 19 + vendor/github.com/dhowden/tag/.travis.yml | 5 + vendor/github.com/dhowden/tag/LICENSE | 23 + vendor/github.com/dhowden/tag/README.md | 72 ++ vendor/github.com/dhowden/tag/dsf.go | 110 +++ vendor/github.com/dhowden/tag/flac.go | 89 +++ vendor/github.com/dhowden/tag/id.go | 81 ++ vendor/github.com/dhowden/tag/id3v1.go | 144 ++++ vendor/github.com/dhowden/tag/id3v2.go | 434 +++++++++++ vendor/github.com/dhowden/tag/id3v2frames.go | 638 ++++++++++++++++ .../github.com/dhowden/tag/id3v2metadata.go | 141 ++++ .../tag/internal/id3v1_test/NOTICE.txt | 3 + vendor/github.com/dhowden/tag/mp4.go | 372 +++++++++ vendor/github.com/dhowden/tag/ogg.go | 119 +++ vendor/github.com/dhowden/tag/sum.go | 219 ++++++ vendor/github.com/dhowden/tag/tag.go | 147 ++++ vendor/github.com/dhowden/tag/util.go | 81 ++ vendor/github.com/dhowden/tag/vorbis.go | 255 +++++++ vendor/github.com/gorilla/mux/.travis.yml | 24 + vendor/github.com/gorilla/mux/AUTHORS | 8 + .../github.com/gorilla/mux/ISSUE_TEMPLATE.md | 11 + vendor/github.com/gorilla/mux/LICENSE | 27 + vendor/github.com/gorilla/mux/README.md | 649 ++++++++++++++++ vendor/github.com/gorilla/mux/context.go | 18 + vendor/github.com/gorilla/mux/doc.go | 306 ++++++++ vendor/github.com/gorilla/mux/go.mod | 1 + vendor/github.com/gorilla/mux/middleware.go | 72 ++ vendor/github.com/gorilla/mux/mux.go | 607 +++++++++++++++ vendor/github.com/gorilla/mux/regexp.go | 345 +++++++++ vendor/github.com/gorilla/mux/route.go | 710 ++++++++++++++++++ vendor/github.com/gorilla/mux/test_helpers.go | 19 + videos/README.md | 1 + 43 files changed, 6221 insertions(+) create mode 100644 .gitignore create mode 100644 Gopkg.lock create mode 100644 Gopkg.toml create mode 100644 main.go create mode 100644 pkg/media/library.go create mode 100644 pkg/media/playlist.go create mode 100644 pkg/media/video.go create mode 100644 static/css/index.css create mode 100644 static/defaulticon.jpg create mode 100644 static/favicon.ico create mode 100644 templates/index.html create mode 100644 vendor/github.com/dhowden/tag/.editorconfig create mode 100644 vendor/github.com/dhowden/tag/.travis.yml create mode 100644 vendor/github.com/dhowden/tag/LICENSE create mode 100644 vendor/github.com/dhowden/tag/README.md create mode 100644 vendor/github.com/dhowden/tag/dsf.go create mode 100644 vendor/github.com/dhowden/tag/flac.go create mode 100644 vendor/github.com/dhowden/tag/id.go create mode 100644 vendor/github.com/dhowden/tag/id3v1.go create mode 100644 vendor/github.com/dhowden/tag/id3v2.go create mode 100644 vendor/github.com/dhowden/tag/id3v2frames.go create mode 100644 vendor/github.com/dhowden/tag/id3v2metadata.go create mode 100644 vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt create mode 100644 vendor/github.com/dhowden/tag/mp4.go create mode 100644 vendor/github.com/dhowden/tag/ogg.go create mode 100644 vendor/github.com/dhowden/tag/sum.go create mode 100644 vendor/github.com/dhowden/tag/tag.go create mode 100644 vendor/github.com/dhowden/tag/util.go create mode 100644 vendor/github.com/dhowden/tag/vorbis.go create mode 100644 vendor/github.com/gorilla/mux/.travis.yml create mode 100644 vendor/github.com/gorilla/mux/AUTHORS create mode 100644 vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md create mode 100644 vendor/github.com/gorilla/mux/LICENSE create mode 100644 vendor/github.com/gorilla/mux/README.md create mode 100644 vendor/github.com/gorilla/mux/context.go create mode 100644 vendor/github.com/gorilla/mux/doc.go create mode 100644 vendor/github.com/gorilla/mux/go.mod create mode 100644 vendor/github.com/gorilla/mux/middleware.go create mode 100644 vendor/github.com/gorilla/mux/mux.go create mode 100644 vendor/github.com/gorilla/mux/regexp.go create mode 100644 vendor/github.com/gorilla/mux/route.go create mode 100644 vendor/github.com/gorilla/mux/test_helpers.go create mode 100644 videos/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d51813c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +videos/* +!videos/README.md \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..8644f48 --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,21 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/dhowden/tag" + packages = ["."] + revision = "db0c67e351b1bfbdfc4f99c911e8afd0ca67de98" + +[[projects]] + name = "github.com/gorilla/mux" + packages = ["."] + revision = "ed099d42384823742bba0bf9a72b53b55c9e2e38" + version = "v1.7.2" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "46ffc0b430cdc7896ae1d51e97b0698ca0cf1df4a0de7ce14e644fa45848491f" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..e6a260c --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,38 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "github.com/dhowden/tag" + +[[constraint]] + name = "github.com/gorilla/mux" + version = "1.7.2" + +[prune] + go-tests = true + unused-packages = true diff --git a/main.go b/main.go new file mode 100644 index 0000000..9a470bc --- /dev/null +++ b/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "html/template" + "log" + "net/http" + + "github.com/gorilla/mux" + "github.com/wybiral/tube/pkg/media" +) + +const addr = "127.0.0.1:40404" + +var templates *template.Template + +var library *media.Library +var playlist media.Playlist + +func main() { + library = media.NewLibrary() + err := library.Import("./videos") + if err != nil { + log.Fatal(err) + } + playlist = library.Playlist() + if len(playlist) == 0 { + log.Fatal("No valid videos found") + } + templates = template.Must(template.ParseGlob("templates/*")) + r := mux.NewRouter().StrictSlash(true) + r.HandleFunc("/", index).Methods("GET") + r.HandleFunc("/v/{id}.mp4", video).Methods("GET") + r.HandleFunc("/t/{id}", thumb).Methods("GET") + r.HandleFunc("/{id}", page).Methods("GET") + r.PathPrefix("/static/").Handler( + http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))), + ).Methods("GET") + log.Printf("Serving at %s", addr) + http.ListenAndServe(addr, r) +} + +func index(w http.ResponseWriter, r *http.Request) { + log.Printf("/index") + http.Redirect(w, r, "/"+playlist[0].ID, 302) +} + +func page(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + log.Printf(id) + playing, ok := library.Videos[id] + if !ok { + log.Print(ok) + return + } + w.Header().Set("Content-Type", "text/html; charset=utf-8") + templates.ExecuteTemplate(w, "index.html", &struct { + Playing *media.Video + Playlist media.Playlist + }{ + Playing: playing, + Playlist: playlist, + }) +} + +func video(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + log.Print("/v/" + id) + m, ok := library.Videos[id] + if !ok { + return + } + title := m.Title + disposition := "attachment; filename=\"" + title + ".mp4\"" + w.Header().Set("Content-Disposition", disposition) + w.Header().Set("Content-Type", "video/mp4") + http.ServeFile(w, r, "./videos/"+id+".mp4") +} + +func thumb(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + id := vars["id"] + log.Printf("/t/" + id) + m, ok := library.Videos[id] + if !ok { + return + } + w.Header().Set("Cache-Control", "public, max-age=7776000") + if m.ThumbType == "" { + w.Header().Set("Content-Type", "image/jpeg") + http.ServeFile(w, r, "static/defaulticon.jpg") + } else { + w.Header().Set("Content-Type", m.ThumbType) + w.Write(m.Thumb) + } +} diff --git a/pkg/media/library.go b/pkg/media/library.go new file mode 100644 index 0000000..b18fe3a --- /dev/null +++ b/pkg/media/library.go @@ -0,0 +1,56 @@ +package media + +import ( + "io/ioutil" + "sort" + "strings" +) + +type Library struct { + Videos map[string]*Video +} + +func NewLibrary() *Library { + lib := &Library{ + Videos: make(map[string]*Video), + } + return lib +} + +func (lib *Library) Import(path string) error { + files, err := ioutil.ReadDir(path) + if err != nil { + return err + } + for _, info := range files { + name := info.Name() + v, err := ParseVideo(path + "/" + name) + if err != nil { + // Ignore files that can't be parsed + continue + } + // Set modified date property + v.Modified = info.ModTime().Format("2006-01-02") + // Default title is filename + if v.Title == "" { + v.Title = name + } + // ID is name without extension + idx := strings.LastIndex(name, ".") + if idx == -1 { + idx = len(name) + } + v.ID = name[:idx] + lib.Videos[v.ID] = v + } + return nil +} + +func (lib *Library) Playlist() Playlist { + pl := make(Playlist, 0) + for _, v := range lib.Videos { + pl = append(pl, v) + } + sort.Sort(pl) + return pl +} diff --git a/pkg/media/playlist.go b/pkg/media/playlist.go new file mode 100644 index 0000000..ed22f27 --- /dev/null +++ b/pkg/media/playlist.go @@ -0,0 +1,15 @@ +package media + +type Playlist []*Video + +func (p Playlist) Len() int { + return len(p) +} + +func (p Playlist) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} + +func (p Playlist) Less(i, j int) bool { + return p[i].ID < p[j].ID +} diff --git a/pkg/media/video.go b/pkg/media/video.go new file mode 100644 index 0000000..bd0def8 --- /dev/null +++ b/pkg/media/video.go @@ -0,0 +1,40 @@ +package media + +import ( + "os" + + "github.com/dhowden/tag" +) + +type Video struct { + ID string + Title string + Album string + Description string + Thumb []byte + ThumbType string + Modified string +} + +func ParseVideo(path string) (*Video, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + m, err := tag.ReadFrom(f) + if err != nil { + return nil, err + } + v := &Video{ + Title: m.Title(), + Album: m.Album(), + Description: m.Comment(), + } + // Add thumbnail (if exists) + p := m.Picture() + if p != nil { + v.Thumb = p.Data + v.ThumbType = p.MIMEType + } + return v, nil +} diff --git a/static/css/index.css b/static/css/index.css new file mode 100644 index 0000000..d73211c --- /dev/null +++ b/static/css/index.css @@ -0,0 +1,166 @@ +/* Normalize */ +* { + font-weight: inherit; + font-size: inherit; + border: none; + outline: none; + margin: 0; + padding: 0; + box-sizing: border-box; +} + +a { + color: inherit; + text-decoration: none; +} + +body { + font-family: Arial, sans-serif; + font-size: 16px; + font-weight: 400; + color: #c5c8c6; + background: #1e1e1e; + padding-bottom: 10px; +} + +nav { + z-index: 100; + color: #ae81ff; + text-shadow: -2px 2px 3px rgba(0, 0, 0, 0.7); + font-weight: 700; + font-size: 20px; + text-indent: 20px; + line-height: 50px; + width: 100%; + height: 50px; + background: #171717; + border-bottom: 1px solid #272727; +} + +main { + width: 1156px; + margin:0 auto; + margin-top: 15px; + white-space: nowrap; +} + +#player { + width: 856px; + display: inline-block; + vertical-align: top; +} + +#video { + width: 100%; + height: 480px; + background: #000; + box-shadow: 0 3px 7px 0 rgba(0, 0, 0, 0.2); +} + +#player > h1 { + margin-top: 10px; +} + +#player > h2 { + margin-top: 5px; + color: #676867; + font-size: 90%; +} + +#player > p { + margin-top: 10px; + font-size: 80%; + width: 100%; + white-space: normal; +} + +#sidebar { + font-size: 13px; + display: inline-block; + margin-left: 10px; + width: 290px; + background: #282a2e; + box-shadow: 0 3px 7px 0 rgba(0, 0, 0, 0.2); +} + +#sidebar > a { + display: block; + padding: 10px; + position: relative; +} + +#sidebar > a:hover { + color: #ae81ff; +} + +#sidebar > a.playing { + background: #383a3e; +} + +#sidebar > a + a { + border-top: 1px solid #1e1e1e; +} + +#sidebar > a > img { + width: 70px; +} + +#sidebar > a > div { + position: absolute; + top: 10px; + right: 10px; + bottom: 10px; + left: 90px; +} + +#sidebar > a > div > h1 { + white-space: normal; +} + +#sidebar > a > div > h2 { + margin-top: 5px; + color: #676867; + font-size: 90%; +} + +@media only screen and (max-width: 1156px) { + main { + width: 940px; + } + #player { + width: 640px; + } + #video { + height: 360px; + } +} + +@media only screen and (max-width: 940px) { + main { + width: 726px; + } + #player { + width: 426px; + } + #video { + height: 240px; + } +} + +@media only screen and (max-width: 726px) { + main { + width: 426px; + } + #player { + width: 426px; + } + #video { + height: 240px; + } + #sidebar { + width: 426px; + margin-top: 10px; + margin-left: 0; + display: block; + } +} diff --git a/static/defaulticon.jpg b/static/defaulticon.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2e99f611f7d4c67478d3d345efd0bfc4aec99ac8 GIT binary patch literal 34694 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ĉr*RGl|0 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 PqYrq{UbL@?R`34+3w;hN literal 0 HcmV?d00001 diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..60b20bb --- /dev/null +++ b/templates/index.html @@ -0,0 +1,36 @@ +{{ $playing := .Playing }} + + + Tube + + + + + + + +
+
+ +

{{ $playing.Title }}

+

{{ $playing.Modified }}

+

{{ $playing.Description }}

+
+ +
+ + diff --git a/vendor/github.com/dhowden/tag/.editorconfig b/vendor/github.com/dhowden/tag/.editorconfig new file mode 100644 index 0000000..57515d0 --- /dev/null +++ b/vendor/github.com/dhowden/tag/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*.go] +indent_style = tab +indent_size = 3 + +[*.md] +trim_trailing_whitespace = false + +[*] +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/vendor/github.com/dhowden/tag/.travis.yml b/vendor/github.com/dhowden/tag/.travis.yml new file mode 100644 index 0000000..3a81bf9 --- /dev/null +++ b/vendor/github.com/dhowden/tag/.travis.yml @@ -0,0 +1,5 @@ +language: go + +go: + - 1.7 + - tip \ No newline at end of file diff --git a/vendor/github.com/dhowden/tag/LICENSE b/vendor/github.com/dhowden/tag/LICENSE new file mode 100644 index 0000000..dfd760c --- /dev/null +++ b/vendor/github.com/dhowden/tag/LICENSE @@ -0,0 +1,23 @@ +Copyright 2015, David Howden +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/dhowden/tag/README.md b/vendor/github.com/dhowden/tag/README.md new file mode 100644 index 0000000..de81883 --- /dev/null +++ b/vendor/github.com/dhowden/tag/README.md @@ -0,0 +1,72 @@ +# MP3/MP4/OGG/FLAC metadata parsing library +[![Build Status](https://travis-ci.org/dhowden/tag.svg?branch=master)](https://travis-ci.org/dhowden/tag) +[![GoDoc](https://godoc.org/github.com/dhowden/tag?status.svg)](https://godoc.org/github.com/dhowden/tag) + +This package provides MP3 (ID3v1,2.{2,3,4}) and MP4 (ACC, M4A, ALAC), OGG and FLAC metadata detection, parsing and artwork extraction. + +Detect and parse tag metadata from an `io.ReadSeeker` (i.e. an `*os.File`): + +```go +m, err := tag.ReadFrom(f) +if err != nil { + log.Fatal(err) +} +log.Print(m.Format()) // The detected format. +log.Print(m.Title()) // The title of the track (see Metadata interface for more details). +``` + +Parsed metadata is exported via a single interface (giving a consistent API for all supported metadata formats). + +```go +// Metadata is an interface which is used to describe metadata retrieved by this package. +type Metadata interface { + Format() Format + FileType() FileType + + Title() string + Album() string + Artist() string + AlbumArtist() string + Composer() string + Genre() string + Year() int + + Track() (int, int) // Number, Total + Disc() (int, int) // Number, Total + + Picture() *Picture // Artwork + Lyrics() string + Comment() string + + Raw() map[string]interface{} // NB: raw tag names are not consistent across formats. +} +``` + +## Audio Data Checksum (SHA1) + +This package also provides a metadata-invariant checksum for audio files: only the audio data is used to +construct the checksum. + +[http://godoc.org/github.com/dhowden/tag#Sum](http://godoc.org/github.com/dhowden/tag#Sum) + +## Tools + +There are simple command-line tools which demonstrate basic tag extraction and summing: + +```console +$ go get github.com/dhowden/tag/... +$ cd $GOPATH/bin +$ ./tag 11\ High\ Hopes.m4a +Metadata Format: MP4 +Title: High Hopes +Album: The Division Bell +Artist: Pink Floyd +Composer: Abbey Road Recording Studios/David Gilmour/Polly Samson +Year: 1994 +Track: 11 of 11 +Disc: 1 of 1 +Picture: Picture{Ext: jpeg, MIMEType: image/jpeg, Type: , Description: , Data.Size: 606109} + +$ ./sum 11\ High\ Hopes.m4a +2ae208c5f00a1f21f5fac9b7f6e0b8e52c06da29 +``` diff --git a/vendor/github.com/dhowden/tag/dsf.go b/vendor/github.com/dhowden/tag/dsf.go new file mode 100644 index 0000000..d826a74 --- /dev/null +++ b/vendor/github.com/dhowden/tag/dsf.go @@ -0,0 +1,110 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "errors" + "io" +) + +// ReadDSFTags reads DSF metadata from the io.ReadSeeker, returning the resulting +// metadata in a Metadata implementation, or non-nil error if there was a problem. +// samples: http://www.2l.no/hires/index.html +func ReadDSFTags(r io.ReadSeeker) (Metadata, error) { + dsd, err := readString(r, 4) + if err != nil { + return nil, err + } + if dsd != "DSD " { + return nil, errors.New("expected 'DSD '") + } + + _, err = r.Seek(int64(16), io.SeekCurrent) + if err != nil { + return nil, err + } + + n4, err := readBytes(r, 8) + if err != nil { + return nil, err + } + id3Pointer := getIntLittleEndian(n4) + + _, err = r.Seek(int64(id3Pointer), io.SeekStart) + if err != nil { + return nil, err + } + + id3, err := ReadID3v2Tags(r) + if err != nil { + return nil, err + } + + return metadataDSF{id3}, nil +} + +type metadataDSF struct { + id3 Metadata +} + +func (m metadataDSF) Format() Format { + return m.id3.Format() +} + +func (m metadataDSF) FileType() FileType { + return DSF +} + +func (m metadataDSF) Title() string { + return m.id3.Title() +} + +func (m metadataDSF) Album() string { + return m.id3.Album() +} + +func (m metadataDSF) Artist() string { + return m.id3.Artist() +} + +func (m metadataDSF) AlbumArtist() string { + return m.id3.AlbumArtist() +} + +func (m metadataDSF) Composer() string { + return m.id3.Composer() +} + +func (m metadataDSF) Year() int { + return m.id3.Year() +} + +func (m metadataDSF) Genre() string { + return m.id3.Genre() +} + +func (m metadataDSF) Track() (int, int) { + return m.id3.Track() +} + +func (m metadataDSF) Disc() (int, int) { + return m.id3.Disc() +} + +func (m metadataDSF) Picture() *Picture { + return m.id3.Picture() +} + +func (m metadataDSF) Lyrics() string { + return m.id3.Lyrics() +} + +func (m metadataDSF) Comment() string { + return m.id3.Comment() +} + +func (m metadataDSF) Raw() map[string]interface{} { + return m.id3.Raw() +} diff --git a/vendor/github.com/dhowden/tag/flac.go b/vendor/github.com/dhowden/tag/flac.go new file mode 100644 index 0000000..c370467 --- /dev/null +++ b/vendor/github.com/dhowden/tag/flac.go @@ -0,0 +1,89 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "errors" + "io" +) + +// blockType is a type which represents an enumeration of valid FLAC blocks +type blockType byte + +// FLAC block types. +const ( + // Stream Info Block 0 + // Padding Block 1 + // Application Block 2 + // Seektable Block 3 + // Cue Sheet Block 5 + vorbisCommentBlock blockType = 4 + pictureBlock blockType = 6 +) + +// ReadFLACTags reads FLAC metadata from the io.ReadSeeker, returning the resulting +// metadata in a Metadata implementation, or non-nil error if there was a problem. +func ReadFLACTags(r io.ReadSeeker) (Metadata, error) { + flac, err := readString(r, 4) + if err != nil { + return nil, err + } + if flac != "fLaC" { + return nil, errors.New("expected 'fLaC'") + } + + m := &metadataFLAC{ + newMetadataVorbis(), + } + + for { + last, err := m.readFLACMetadataBlock(r) + if err != nil { + return nil, err + } + + if last { + break + } + } + return m, nil +} + +type metadataFLAC struct { + *metadataVorbis +} + +func (m *metadataFLAC) readFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) { + blockHeader, err := readBytes(r, 1) + if err != nil { + return + } + + if getBit(blockHeader[0], 7) { + blockHeader[0] ^= (1 << 7) + last = true + } + + blockLen, err := readInt(r, 3) + if err != nil { + return + } + + switch blockType(blockHeader[0]) { + case vorbisCommentBlock: + err = m.readVorbisComment(r) + + case pictureBlock: + err = m.readPictureBlock(r) + + default: + _, err = r.Seek(int64(blockLen), io.SeekCurrent) + } + return +} + +func (m *metadataFLAC) FileType() FileType { + return FLAC +} diff --git a/vendor/github.com/dhowden/tag/id.go b/vendor/github.com/dhowden/tag/id.go new file mode 100644 index 0000000..2410356 --- /dev/null +++ b/vendor/github.com/dhowden/tag/id.go @@ -0,0 +1,81 @@ +package tag + +import ( + "fmt" + "io" +) + +// Identify identifies the format and file type of the data in the ReadSeeker. +func Identify(r io.ReadSeeker) (format Format, fileType FileType, err error) { + b, err := readBytes(r, 11) + if err != nil { + return + } + + _, err = r.Seek(-11, io.SeekCurrent) + if err != nil { + err = fmt.Errorf("could not seek back to original position: %v", err) + return + } + + switch { + case string(b[0:4]) == "fLaC": + return VORBIS, FLAC, nil + + case string(b[0:4]) == "OggS": + return VORBIS, OGG, nil + + case string(b[4:8]) == "ftyp": + b = b[8:11] + fileType = UnknownFileType + switch string(b) { + case "M4A": + fileType = M4A + + case "M4B": + fileType = M4B + + case "M4P": + fileType = M4P + } + return MP4, fileType, nil + + case string(b[0:3]) == "ID3": + b := b[3:] + switch uint(b[0]) { + case 2: + format = ID3v2_2 + case 3: + format = ID3v2_3 + case 4: + format = ID3v2_4 + case 0, 1: + fallthrough + default: + err = fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0])) + return + } + return format, MP3, nil + } + + n, err := r.Seek(-128, io.SeekEnd) + if err != nil { + return + } + + tag, err := readString(r, 3) + if err != nil { + return + } + + _, err = r.Seek(-n, io.SeekCurrent) + if err != nil { + return + } + + if tag != "TAG" { + err = ErrNoTagsFound + return + } + return ID3v1, MP3, nil +} diff --git a/vendor/github.com/dhowden/tag/id3v1.go b/vendor/github.com/dhowden/tag/id3v1.go new file mode 100644 index 0000000..0953f0b --- /dev/null +++ b/vendor/github.com/dhowden/tag/id3v1.go @@ -0,0 +1,144 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "errors" + "io" + "strconv" + "strings" +) + +// id3v1Genres is a list of genres as given in the ID3v1 specification. +var id3v1Genres = [...]string{ + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", + "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", + "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", + "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", + "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", + "Cabaret", "New Wave", "Psychadelic", "Rave", "Showtunes", "Trailer", + "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", + "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", + "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", + "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", + "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", + "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", + "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", + "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", + "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", + "Duet", "Punk Rock", "Drum Solo", "Acapella", "Euro-House", "Dance Hall", +} + +// ErrNotID3v1 is an error which is returned when no ID3v1 header is found. +var ErrNotID3v1 = errors.New("invalid ID3v1 header") + +// ReadID3v1Tags reads ID3v1 tags from the io.ReadSeeker. Returns ErrNotID3v1 +// if there are no ID3v1 tags, otherwise non-nil error if there was a problem. +func ReadID3v1Tags(r io.ReadSeeker) (Metadata, error) { + _, err := r.Seek(-128, io.SeekEnd) + if err != nil { + return nil, err + } + + if tag, err := readString(r, 3); err != nil { + return nil, err + } else if tag != "TAG" { + return nil, ErrNotID3v1 + } + + title, err := readString(r, 30) + if err != nil { + return nil, err + } + + artist, err := readString(r, 30) + if err != nil { + return nil, err + } + + album, err := readString(r, 30) + if err != nil { + return nil, err + } + + year, err := readString(r, 4) + if err != nil { + return nil, err + } + + commentBytes, err := readBytes(r, 30) + if err != nil { + return nil, err + } + + var comment string + var track int + if commentBytes[28] == 0 { + comment = trimString(string(commentBytes[:28])) + track = int(commentBytes[29]) + } else { + comment = trimString(string(commentBytes)) + } + + var genre string + genreID, err := readBytes(r, 1) + if err != nil { + return nil, err + } + if int(genreID[0]) < len(id3v1Genres) { + genre = id3v1Genres[int(genreID[0])] + } + + m := make(map[string]interface{}) + m["title"] = trimString(title) + m["artist"] = trimString(artist) + m["album"] = trimString(album) + m["year"] = trimString(year) + m["comment"] = trimString(comment) + m["track"] = track + m["genre"] = genre + + return metadataID3v1(m), nil +} + +func trimString(x string) string { + return strings.TrimSpace(strings.Trim(x, "\x00")) +} + +// metadataID3v1 is the implementation of Metadata used for ID3v1 tags. +type metadataID3v1 map[string]interface{} + +func (metadataID3v1) Format() Format { return ID3v1 } +func (metadataID3v1) FileType() FileType { return MP3 } +func (m metadataID3v1) Raw() map[string]interface{} { return m } + +func (m metadataID3v1) Title() string { return m["title"].(string) } +func (m metadataID3v1) Album() string { return m["album"].(string) } +func (m metadataID3v1) Artist() string { return m["artist"].(string) } +func (m metadataID3v1) Genre() string { return m["genre"].(string) } + +func (m metadataID3v1) Year() int { + y := m["year"].(string) + n, err := strconv.Atoi(y) + if err != nil { + return 0 + } + return n +} + +func (m metadataID3v1) Track() (int, int) { return m["track"].(int), 0 } + +func (m metadataID3v1) AlbumArtist() string { return "" } +func (m metadataID3v1) Composer() string { return "" } +func (metadataID3v1) Disc() (int, int) { return 0, 0 } +func (m metadataID3v1) Picture() *Picture { return nil } +func (m metadataID3v1) Lyrics() string { return "" } +func (m metadataID3v1) Comment() string { return m["comment"].(string) } diff --git a/vendor/github.com/dhowden/tag/id3v2.go b/vendor/github.com/dhowden/tag/id3v2.go new file mode 100644 index 0000000..063e6cb --- /dev/null +++ b/vendor/github.com/dhowden/tag/id3v2.go @@ -0,0 +1,434 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "fmt" + "io" + "regexp" + "strconv" + "strings" +) + +var id3v2Genres = [...]string{ + "Blues", "Classic Rock", "Country", "Dance", "Disco", "Funk", "Grunge", + "Hip-Hop", "Jazz", "Metal", "New Age", "Oldies", "Other", "Pop", "R&B", + "Rap", "Reggae", "Rock", "Techno", "Industrial", "Alternative", "Ska", + "Death Metal", "Pranks", "Soundtrack", "Euro-Techno", "Ambient", + "Trip-Hop", "Vocal", "Jazz+Funk", "Fusion", "Trance", "Classical", + "Instrumental", "Acid", "House", "Game", "Sound Clip", "Gospel", + "Noise", "AlternRock", "Bass", "Soul", "Punk", "Space", "Meditative", + "Instrumental Pop", "Instrumental Rock", "Ethnic", "Gothic", + "Darkwave", "Techno-Industrial", "Electronic", "Pop-Folk", + "Eurodance", "Dream", "Southern Rock", "Comedy", "Cult", "Gangsta", + "Top 40", "Christian Rap", "Pop/Funk", "Jungle", "Native American", + "Cabaret", "New Wave", "Psychedelic", "Rave", "Showtunes", "Trailer", + "Lo-Fi", "Tribal", "Acid Punk", "Acid Jazz", "Polka", "Retro", + "Musical", "Rock & Roll", "Hard Rock", "Folk", "Folk-Rock", + "National Folk", "Swing", "Fast Fusion", "Bebob", "Latin", "Revival", + "Celtic", "Bluegrass", "Avantgarde", "Gothic Rock", "Progressive Rock", + "Psychedelic Rock", "Symphonic Rock", "Slow Rock", "Big Band", + "Chorus", "Easy Listening", "Acoustic", "Humour", "Speech", "Chanson", + "Opera", "Chamber Music", "Sonata", "Symphony", "Booty Bass", "Primus", + "Porn Groove", "Satire", "Slow Jam", "Club", "Tango", "Samba", + "Folklore", "Ballad", "Power Ballad", "Rhythmic Soul", "Freestyle", + "Duet", "Punk Rock", "Drum Solo", "A capella", "Euro-House", "Dance Hall", + "Goa", "Drum & Bass", "Club-House", "Hardcore", "Terror", "Indie", + "Britpop", "Negerpunk", "Polsk Punk", "Beat", "Christian Gangsta Rap", + "Heavy Metal", "Black Metal", "Crossover", "Contemporary Christian", + "Christian Rock ", "Merengue", "Salsa", "Thrash Metal", "Anime", "JPop", + "Synthpop", +} + +// id3v2Header is a type which represents an ID3v2 tag header. +type id3v2Header struct { + Version Format + Unsynchronisation bool + ExtendedHeader bool + Experimental bool + Size int +} + +// readID3v2Header reads the ID3v2 header from the given io.Reader. +// offset it number of bytes of header that was read +func readID3v2Header(r io.Reader) (h *id3v2Header, offset int, err error) { + offset = 10 + b, err := readBytes(r, offset) + if err != nil { + return nil, 0, fmt.Errorf("expected to read 10 bytes (ID3v2Header): %v", err) + } + + if string(b[0:3]) != "ID3" { + return nil, 0, fmt.Errorf("expected to read \"ID3\"") + } + + b = b[3:] + var vers Format + switch uint(b[0]) { + case 2: + vers = ID3v2_2 + case 3: + vers = ID3v2_3 + case 4: + vers = ID3v2_4 + case 0, 1: + fallthrough + default: + return nil, 0, fmt.Errorf("ID3 version: %v, expected: 2, 3 or 4", uint(b[0])) + } + + // NB: We ignore b[1] (the revision) as we don't currently rely on it. + h = &id3v2Header{ + Version: vers, + Unsynchronisation: getBit(b[2], 7), + ExtendedHeader: getBit(b[2], 6), + Experimental: getBit(b[2], 5), + Size: get7BitChunkedInt(b[3:7]), + } + + if h.ExtendedHeader { + switch vers { + case ID3v2_3: + b, err := readBytes(r, 4) + if err != nil { + return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v23 extended header len): %v", err) + } + // skip header, size is excluding len bytes + extendedHeaderSize := getInt(b) + _, err = readBytes(r, extendedHeaderSize) + if err != nil { + return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v23 skip extended header): %v", extendedHeaderSize, err) + } + offset += extendedHeaderSize + case ID3v2_4: + b, err := readBytes(r, 4) + if err != nil { + return nil, 0, fmt.Errorf("expected to read 4 bytes (ID3v24 extended header len): %v", err) + } + // skip header, size is synchsafe int including len bytes + extendedHeaderSize := get7BitChunkedInt(b) - 4 + _, err = readBytes(r, extendedHeaderSize) + if err != nil { + return nil, 0, fmt.Errorf("expected to read %d bytes (ID3v24 skip extended header): %v", extendedHeaderSize, err) + } + offset += extendedHeaderSize + default: + // nop, only 2.3 and 2.4 should have extended header + } + } + + return h, offset, nil +} + +// id3v2FrameFlags is a type which represents the flags which can be set on an ID3v2 frame. +type id3v2FrameFlags struct { + // Message (ID3 2.3.0 and 2.4.0) + TagAlterPreservation bool + FileAlterPreservation bool + ReadOnly bool + + // Format (ID3 2.3.0 and 2.4.0) + Compression bool + Encryption bool + GroupIdentity bool + // ID3 2.4.0 only (see http://id3.org/id3v2.4.0-structure sec 4.1) + Unsynchronisation bool + DataLengthIndicator bool +} + +func readID3v23FrameFlags(r io.Reader) (*id3v2FrameFlags, error) { + b, err := readBytes(r, 2) + if err != nil { + return nil, err + } + + msg := b[0] + fmt := b[1] + + return &id3v2FrameFlags{ + TagAlterPreservation: getBit(msg, 7), + FileAlterPreservation: getBit(msg, 6), + ReadOnly: getBit(msg, 5), + Compression: getBit(fmt, 7), + Encryption: getBit(fmt, 6), + GroupIdentity: getBit(fmt, 5), + }, nil +} + +func readID3v24FrameFlags(r io.Reader) (*id3v2FrameFlags, error) { + b, err := readBytes(r, 2) + if err != nil { + return nil, err + } + + msg := b[0] + fmt := b[1] + + return &id3v2FrameFlags{ + TagAlterPreservation: getBit(msg, 6), + FileAlterPreservation: getBit(msg, 5), + ReadOnly: getBit(msg, 4), + GroupIdentity: getBit(fmt, 6), + Compression: getBit(fmt, 3), + Encryption: getBit(fmt, 2), + Unsynchronisation: getBit(fmt, 1), + DataLengthIndicator: getBit(fmt, 0), + }, nil + +} + +func readID3v2_2FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) { + name, err = readString(r, 3) + if err != nil { + return + } + size, err = readInt(r, 3) + if err != nil { + return + } + headerSize = 6 + return +} + +func readID3v2_3FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) { + name, err = readString(r, 4) + if err != nil { + return + } + size, err = readInt(r, 4) + if err != nil { + return + } + headerSize = 8 + return +} + +func readID3v2_4FrameHeader(r io.Reader) (name string, size int, headerSize int, err error) { + name, err = readString(r, 4) + if err != nil { + return + } + size, err = read7BitChunkedInt(r, 4) + if err != nil { + return + } + headerSize = 8 + return +} + +// readID3v2Frames reads ID3v2 frames from the given reader using the ID3v2Header. +func readID3v2Frames(r io.Reader, offset int, h *id3v2Header) (map[string]interface{}, error) { + result := make(map[string]interface{}) + + for offset < h.Size { + var err error + var name string + var size, headerSize int + var flags *id3v2FrameFlags + + switch h.Version { + case ID3v2_2: + name, size, headerSize, err = readID3v2_2FrameHeader(r) + + case ID3v2_3: + name, size, headerSize, err = readID3v2_3FrameHeader(r) + if err != nil { + return nil, err + } + flags, err = readID3v23FrameFlags(r) + headerSize += 2 + + case ID3v2_4: + name, size, headerSize, err = readID3v2_4FrameHeader(r) + if err != nil { + return nil, err + } + flags, err = readID3v24FrameFlags(r) + headerSize += 2 + } + + if err != nil { + return nil, err + } + + // FIXME: Do we still need this? + // if size=0, we certainly are in a padding zone. ignore the rest of + // the tags + if size == 0 { + break + } + + offset += headerSize + size + + // Avoid corrupted padding (see http://id3.org/Compliance%20Issues). + if !validID3Frame(h.Version, name) && offset > h.Size { + break + } + + if flags != nil { + if flags.Compression { + _, err = read7BitChunkedInt(r, 4) // read 4 + if err != nil { + return nil, err + } + size -= 4 + } + + if flags.Encryption { + _, err = readBytes(r, 1) // read 1 byte of encryption method + if err != nil { + return nil, err + } + size -= 1 + } + } + + b, err := readBytes(r, size) + if err != nil { + return nil, err + } + + // There can be multiple tag with the same name. Append a number to the + // name if there is more than one. + rawName := name + if _, ok := result[rawName]; ok { + for i := 0; ok; i++ { + rawName = name + "_" + strconv.Itoa(i) + _, ok = result[rawName] + } + } + + switch { + case name == "TXXX" || name == "TXX": + t, err := readTextWithDescrFrame(b, false, true) // no lang, but enc + if err != nil { + return nil, err + } + result[rawName] = t + + case name[0] == 'T': + txt, err := readTFrame(b) + if err != nil { + return nil, err + } + result[rawName] = txt + + case name == "UFID" || name == "UFI": + t, err := readUFID(b) + if err != nil { + return nil, err + } + result[rawName] = t + + case name == "WXXX" || name == "WXX": + t, err := readTextWithDescrFrame(b, false, false) // no lang, no enc + if err != nil { + return nil, err + } + result[rawName] = t + + case name[0] == 'W': + txt, err := readWFrame(b) + if err != nil { + return nil, err + } + result[rawName] = txt + + case name == "COMM" || name == "COM" || name == "USLT" || name == "ULT": + t, err := readTextWithDescrFrame(b, true, true) // both lang and enc + if err != nil { + return nil, err + } + result[rawName] = t + + case name == "APIC": + p, err := readAPICFrame(b) + if err != nil { + return nil, err + } + result[rawName] = p + + case name == "PIC": + p, err := readPICFrame(b) + if err != nil { + return nil, err + } + result[rawName] = p + + default: + result[rawName] = b + } + } + return result, nil +} + +type unsynchroniser struct { + io.Reader + ff bool +} + +// filter io.Reader which skip the Unsynchronisation bytes +func (r *unsynchroniser) Read(p []byte) (int, error) { + b := make([]byte, 1) + i := 0 + for i < len(p) { + if n, err := r.Reader.Read(b); err != nil || n == 0 { + return i, err + } + if r.ff && b[0] == 0x00 { + r.ff = false + continue + } + p[i] = b[0] + i++ + r.ff = (b[0] == 0xFF) + } + return i, nil +} + +// ReadID3v2Tags parses ID3v2.{2,3,4} tags from the io.ReadSeeker into a Metadata, returning +// non-nil error on failure. +func ReadID3v2Tags(r io.ReadSeeker) (Metadata, error) { + h, offset, err := readID3v2Header(r) + if err != nil { + return nil, err + } + + var ur io.Reader = r + if h.Unsynchronisation { + ur = &unsynchroniser{Reader: r} + } + + f, err := readID3v2Frames(ur, offset, h) + if err != nil { + return nil, err + } + return metadataID3v2{header: h, frames: f}, nil +} + +var id3v2genreRe = regexp.MustCompile(`(.*[^(]|.* |^)\(([0-9]+)\) *(.*)$`) + +// id3v2genre parse a id3v2 genre tag and expand the numeric genres +func id3v2genre(genre string) string { + c := true + for c { + orig := genre + if match := id3v2genreRe.FindStringSubmatch(genre); len(match) > 0 { + if genreID, err := strconv.Atoi(match[2]); err == nil { + if genreID < len(id3v2Genres) { + genre = id3v2Genres[genreID] + if match[1] != "" { + genre = strings.TrimSpace(match[1]) + " " + genre + } + if match[3] != "" { + genre = genre + " " + match[3] + } + } + } + } + c = (orig != genre) + } + return strings.Replace(genre, "((", "(", -1) +} diff --git a/vendor/github.com/dhowden/tag/id3v2frames.go b/vendor/github.com/dhowden/tag/id3v2frames.go new file mode 100644 index 0000000..a92afa1 --- /dev/null +++ b/vendor/github.com/dhowden/tag/id3v2frames.go @@ -0,0 +1,638 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strings" + "unicode/utf16" +) + +// DefaultUTF16WithBOMByteOrder is the byte order used when the "UTF16 with BOM" encoding +// is specified without a corresponding BOM in the data. +var DefaultUTF16WithBOMByteOrder binary.ByteOrder = binary.LittleEndian + +// ID3v2.2.0 frames (see http://id3.org/id3v2-00, sec 4). +var id3v22Frames = map[string]string{ + "BUF": "Recommended buffer size", + + "CNT": "Play counter", + "COM": "Comments", + "CRA": "Audio encryption", + "CRM": "Encrypted meta frame", + + "ETC": "Event timing codes", + "EQU": "Equalization", + + "GEO": "General encapsulated object", + + "IPL": "Involved people list", + + "LNK": "Linked information", + + "MCI": "Music CD Identifier", + "MLL": "MPEG location lookup table", + + "PIC": "Attached picture", + "POP": "Popularimeter", + + "REV": "Reverb", + "RVA": "Relative volume adjustment", + + "SLT": "Synchronized lyric/text", + "STC": "Synced tempo codes", + + "TAL": "Album/Movie/Show title", + "TBP": "BPM (Beats Per Minute)", + "TCM": "Composer", + "TCO": "Content type", + "TCR": "Copyright message", + "TDA": "Date", + "TDY": "Playlist delay", + "TEN": "Encoded by", + "TFT": "File type", + "TIM": "Time", + "TKE": "Initial key", + "TLA": "Language(s)", + "TLE": "Length", + "TMT": "Media type", + "TOA": "Original artist(s)/performer(s)", + "TOF": "Original filename", + "TOL": "Original Lyricist(s)/text writer(s)", + "TOR": "Original release year", + "TOT": "Original album/Movie/Show title", + "TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", + "TP2": "Band/Orchestra/Accompaniment", + "TP3": "Conductor/Performer refinement", + "TP4": "Interpreted, remixed, or otherwise modified by", + "TPA": "Part of a set", + "TPB": "Publisher", + "TRC": "ISRC (International Standard Recording Code)", + "TRD": "Recording dates", + "TRK": "Track number/Position in set", + "TSI": "Size", + "TSS": "Software/hardware and settings used for encoding", + "TT1": "Content group description", + "TT2": "Title/Songname/Content description", + "TT3": "Subtitle/Description refinement", + "TXT": "Lyricist/text writer", + "TXX": "User defined text information frame", + "TYE": "Year", + + "UFI": "Unique file identifier", + "ULT": "Unsychronized lyric/text transcription", + + "WAF": "Official audio file webpage", + "WAR": "Official artist/performer webpage", + "WAS": "Official audio source webpage", + "WCM": "Commercial information", + "WCP": "Copyright/Legal information", + "WPB": "Publishers official webpage", + "WXX": "User defined URL link frame", +} + +// ID3v2.3.0 frames (see http://id3.org/id3v2.3.0#Declared_ID3v2_frames). +var id3v23Frames = map[string]string{ + "AENC": "Audio encryption]", + "APIC": "Attached picture", + "COMM": "Comments", + "COMR": "Commercial frame", + "ENCR": "Encryption method registration", + "EQUA": "Equalization", + "ETCO": "Event timing codes", + "GEOB": "General encapsulated object", + "GRID": "Group identification registration", + "IPLS": "Involved people list", + "LINK": "Linked information", + "MCDI": "Music CD identifier", + "MLLT": "MPEG location lookup table", + "OWNE": "Ownership frame", + "PRIV": "Private frame", + "PCNT": "Play counter", + "POPM": "Popularimeter", + "POSS": "Position synchronisation frame", + "RBUF": "Recommended buffer size", + "RVAD": "Relative volume adjustment", + "RVRB": "Reverb", + "SYLT": "Synchronized lyric/text", + "SYTC": "Synchronized tempo codes", + "TALB": "Album/Movie/Show title", + "TBPM": "BPM (beats per minute)", + "TCMP": "iTunes Compilation Flag", + "TCOM": "Composer", + "TCON": "Content type", + "TCOP": "Copyright message", + "TDAT": "Date", + "TDLY": "Playlist delay", + "TENC": "Encoded by", + "TEXT": "Lyricist/Text writer", + "TFLT": "File type", + "TIME": "Time", + "TIT1": "Content group description", + "TIT2": "Title/songname/content description", + "TIT3": "Subtitle/Description refinement", + "TKEY": "Initial key", + "TLAN": "Language(s)", + "TLEN": "Length", + "TMED": "Media type", + "TOAL": "Original album/movie/show title", + "TOFN": "Original filename", + "TOLY": "Original lyricist(s)/text writer(s)", + "TOPE": "Original artist(s)/performer(s)", + "TORY": "Original release year", + "TOWN": "File owner/licensee", + "TPE1": "Lead performer(s)/Soloist(s)", + "TPE2": "Band/orchestra/accompaniment", + "TPE3": "Conductor/performer refinement", + "TPE4": "Interpreted, remixed, or otherwise modified by", + "TPOS": "Part of a set", + "TPUB": "Publisher", + "TRCK": "Track number/Position in set", + "TRDA": "Recording dates", + "TRSN": "Internet radio station name", + "TRSO": "Internet radio station owner", + "TSIZ": "Size", + "TSO2": "iTunes uses this for Album Artist sort order", + "TSOC": "iTunes uses this for Composer sort order", + "TSRC": "ISRC (international standard recording code)", + "TSSE": "Software/Hardware and settings used for encoding", + "TYER": "Year", + "TXXX": "User defined text information frame", + "UFID": "Unique file identifier", + "USER": "Terms of use", + "USLT": "Unsychronized lyric/text transcription", + "WCOM": "Commercial information", + "WCOP": "Copyright/Legal information", + "WOAF": "Official audio file webpage", + "WOAR": "Official artist/performer webpage", + "WOAS": "Official audio source webpage", + "WORS": "Official internet radio station homepage", + "WPAY": "Payment", + "WPUB": "Publishers official webpage", + "WXXX": "User defined URL link frame", +} + +// ID3v2.4.0 frames (see http://id3.org/id3v2.4.0-frames, sec 4). +var id3v24Frames = map[string]string{ + "AENC": "Audio encryption", + "APIC": "Attached picture", + "ASPI": "Audio seek point index", + + "COMM": "Comments", + "COMR": "Commercial frame", + + "ENCR": "Encryption method registration", + "EQU2": "Equalisation (2)", + "ETCO": "Event timing codes", + + "GEOB": "General encapsulated object", + "GRID": "Group identification registration", + + "LINK": "Linked information", + + "MCDI": "Music CD identifier", + "MLLT": "MPEG location lookup table", + + "OWNE": "Ownership frame", + + "PRIV": "Private frame", + "PCNT": "Play counter", + "POPM": "Popularimeter", + "POSS": "Position synchronisation frame", + + "RBUF": "Recommended buffer size", + "RVA2": "Relative volume adjustment (2)", + "RVRB": "Reverb", + + "SEEK": "Seek frame", + "SIGN": "Signature frame", + "SYLT": "Synchronised lyric/text", + "SYTC": "Synchronised tempo codes", + + "TALB": "Album/Movie/Show title", + "TBPM": "BPM (beats per minute)", + "TCMP": "iTunes Compilation Flag", + "TCOM": "Composer", + "TCON": "Content type", + "TCOP": "Copyright message", + "TDEN": "Encoding time", + "TDLY": "Playlist delay", + "TDOR": "Original release time", + "TDRC": "Recording time", + "TDRL": "Release time", + "TDTG": "Tagging time", + "TENC": "Encoded by", + "TEXT": "Lyricist/Text writer", + "TFLT": "File type", + "TIPL": "Involved people list", + "TIT1": "Content group description", + "TIT2": "Title/songname/content description", + "TIT3": "Subtitle/Description refinement", + "TKEY": "Initial key", + "TLAN": "Language(s)", + "TLEN": "Length", + "TMCL": "Musician credits list", + "TMED": "Media type", + "TMOO": "Mood", + "TOAL": "Original album/movie/show title", + "TOFN": "Original filename", + "TOLY": "Original lyricist(s)/text writer(s)", + "TOPE": "Original artist(s)/performer(s)", + "TOWN": "File owner/licensee", + "TPE1": "Lead performer(s)/Soloist(s)", + "TPE2": "Band/orchestra/accompaniment", + "TPE3": "Conductor/performer refinement", + "TPE4": "Interpreted, remixed, or otherwise modified by", + "TPOS": "Part of a set", + "TPRO": "Produced notice", + "TPUB": "Publisher", + "TRCK": "Track number/Position in set", + "TRSN": "Internet radio station name", + "TRSO": "Internet radio station owner", + "TSO2": "iTunes uses this for Album Artist sort order", + "TSOA": "Album sort order", + "TSOC": "iTunes uses this for Composer sort order", + "TSOP": "Performer sort order", + "TSOT": "Title sort order", + "TSRC": "ISRC (international standard recording code)", + "TSSE": "Software/Hardware and settings used for encoding", + "TSST": "Set subtitle", + "TXXX": "User defined text information frame", + + "UFID": "Unique file identifier", + "USER": "Terms of use", + "USLT": "Unsynchronised lyric/text transcription", + + "WCOM": "Commercial information", + "WCOP": "Copyright/Legal information", + "WOAF": "Official audio file webpage", + "WOAR": "Official artist/performer webpage", + "WOAS": "Official audio source webpage", + "WORS": "Official Internet radio station homepage", + "WPAY": "Payment", + "WPUB": "Publishers official webpage", + "WXXX": "User defined URL link frame", +} + +// ID3 frames that are defined in the specs. +var id3Frames = map[Format]map[string]string{ + ID3v2_2: id3v22Frames, + ID3v2_3: id3v23Frames, + ID3v2_4: id3v24Frames, +} + +func validID3Frame(version Format, name string) bool { + names, ok := id3Frames[version] + if !ok { + return false + } + _, ok = names[name] + return ok +} + +func readWFrame(b []byte) (string, error) { + // Frame text is always encoded in ISO-8859-1 + b = append([]byte{0}, b...) + return readTFrame(b) +} + +func readTFrame(b []byte) (string, error) { + if len(b) == 0 { + return "", nil + } + + txt, err := decodeText(b[0], b[1:]) + if err != nil { + return "", err + } + return strings.Join(strings.Split(txt, string(singleZero)), ""), nil +} + +const ( + encodingISO8859 byte = 0 + encodingUTF16WithBOM byte = 1 + encodingUTF16 byte = 2 + encodingUTF8 byte = 3 +) + +func decodeText(enc byte, b []byte) (string, error) { + if len(b) == 0 { + return "", nil + } + + switch enc { + case encodingISO8859: // ISO-8859-1 + return decodeISO8859(b), nil + + case encodingUTF16WithBOM: // UTF-16 with byte order marker + if len(b) == 1 { + return "", nil + } + return decodeUTF16WithBOM(b) + + case encodingUTF16: // UTF-16 without byte order (assuming BigEndian) + if len(b) == 1 { + return "", nil + } + return decodeUTF16(b, binary.BigEndian) + + case encodingUTF8: // UTF-8 + return string(b), nil + + default: // Fallback to ISO-8859-1 + return decodeISO8859(b), nil + } +} + +var ( + singleZero = []byte{0} + doubleZero = []byte{0, 0} +) + +func dataSplit(b []byte, enc byte) [][]byte { + delim := singleZero + if enc == encodingUTF16 || enc == encodingUTF16WithBOM { + delim = doubleZero + } + + result := bytes.SplitN(b, delim, 2) + if len(result) != 2 { + return result + } + + if len(result[1]) == 0 { + return result + } + + if result[1][0] == 0 { + // there was a double (or triple) 0 and we cut too early + result[0] = append(result[0], result[1][0]) + result[1] = result[1][1:] + } + return result +} + +func decodeISO8859(b []byte) string { + r := make([]rune, len(b)) + for i, x := range b { + r[i] = rune(x) + } + return string(r) +} + +func decodeUTF16WithBOM(b []byte) (string, error) { + if len(b) < 2 { + return "", errors.New("invalid encoding: expected at least 2 bytes for UTF-16 byte order mark") + } + + var bo binary.ByteOrder + switch { + case b[0] == 0xFE && b[1] == 0xFF: + bo = binary.BigEndian + b = b[2:] + + case b[0] == 0xFF && b[1] == 0xFE: + bo = binary.LittleEndian + b = b[2:] + + default: + bo = DefaultUTF16WithBOMByteOrder + } + return decodeUTF16(b, bo) +} + +func decodeUTF16(b []byte, bo binary.ByteOrder) (string, error) { + if len(b)%2 != 0 { + return "", errors.New("invalid encoding: expected even number of bytes for UTF-16 encoded text") + } + s := make([]uint16, 0, len(b)/2) + for i := 0; i < len(b); i += 2 { + s = append(s, bo.Uint16(b[i:i+2])) + } + return string(utf16.Decode(s)), nil +} + +// Comm is a type used in COMM, UFID, TXXX, WXXX and USLT tag. +// It's a text with a description and a specified language +// For WXXX, TXXX and UFID, we don't set a Language +type Comm struct { + Language string + Description string + Text string +} + +// String returns a string representation of the underlying Comm instance. +func (t Comm) String() string { + if t.Language != "" { + return fmt.Sprintf("Text{Lang: '%v', Description: '%v', %v lines}", + t.Language, t.Description, strings.Count(t.Text, "\n")) + } + return fmt.Sprintf("Text{Description: '%v', %v}", t.Description, t.Text) +} + +// IDv2.{3,4} +// -- Header +//
+//
+// -- readTextWithDescrFrame(data, true, true) +// Text encoding $xx +// Language $xx xx xx +// Content descriptor $00 (00) +// Lyrics/text +// -- Header +//
+//
+// -- readTextWithDescrFrame(data, false, ) +// Text encoding $xx +// Description $00 (00) +// Value +func readTextWithDescrFrame(b []byte, hasLang bool, encoded bool) (*Comm, error) { + enc := b[0] + b = b[1:] + + c := &Comm{} + if hasLang { + c.Language = string(b[:3]) + b = b[3:] + } + + descTextSplit := dataSplit(b, enc) + if len(descTextSplit) < 1 { + return nil, fmt.Errorf("error decoding tag description text: invalid encoding") + } + + desc, err := decodeText(enc, descTextSplit[0]) + if err != nil { + return nil, fmt.Errorf("error decoding tag description text: %v", err) + } + c.Description = desc + + if len(descTextSplit) == 1 { + return c, nil + } + + if !encoded { + enc = byte(0) + } + text, err := decodeText(enc, descTextSplit[1]) + if err != nil { + return nil, fmt.Errorf("error decoding tag text: %v", err) + } + c.Text = text + + return c, nil +} + +// UFID is composed of a provider (frequently a URL and a binary identifier) +// The identifier can be a text (Musicbrainz use texts, but not necessary) +type UFID struct { + Provider string + Identifier []byte +} + +func (u UFID) String() string { + return fmt.Sprintf("%v (%v)", u.Provider, string(u.Identifier)) +} + +func readUFID(b []byte) (*UFID, error) { + result := bytes.SplitN(b, singleZero, 2) + if len(result) != 2 { + return nil, errors.New("expected to split UFID data into 2 pieces") + } + + return &UFID{ + Provider: string(result[0]), + Identifier: result[1], + }, nil +} + +var pictureTypes = map[byte]string{ + 0x00: "Other", + 0x01: "32x32 pixels 'file icon' (PNG only)", + 0x02: "Other file icon", + 0x03: "Cover (front)", + 0x04: "Cover (back)", + 0x05: "Leaflet page", + 0x06: "Media (e.g. lable side of CD)", + 0x07: "Lead artist/lead performer/soloist", + 0x08: "Artist/performer", + 0x09: "Conductor", + 0x0A: "Band/Orchestra", + 0x0B: "Composer", + 0x0C: "Lyricist/text writer", + 0x0D: "Recording Location", + 0x0E: "During recording", + 0x0F: "During performance", + 0x10: "Movie/video screen capture", + 0x11: "A bright coloured fish", + 0x12: "Illustration", + 0x13: "Band/artist logotype", + 0x14: "Publisher/Studio logotype", +} + +// Picture is a type which represents an attached picture extracted from metadata. +type Picture struct { + Ext string // Extension of the picture file. + MIMEType string // MIMEType of the picture. + Type string // Type of the picture (see pictureTypes). + Description string // Description. + Data []byte // Raw picture data. +} + +// String returns a string representation of the underlying Picture instance. +func (p Picture) String() string { + return fmt.Sprintf("Picture{Ext: %v, MIMEType: %v, Type: %v, Description: %v, Data.Size: %v}", + p.Ext, p.MIMEType, p.Type, p.Description, len(p.Data)) +} + +// IDv2.2 +// -- Header +// Attached picture "PIC" +// Frame size $xx xx xx +// -- readPICFrame +// Text encoding $xx +// Image format $xx xx xx +// Picture type $xx +// Description $00 (00) +// Picture data +func readPICFrame(b []byte) (*Picture, error) { + enc := b[0] + ext := string(b[1:4]) + picType := b[4] + + descDataSplit := dataSplit(b[5:], enc) + if len(descDataSplit) != 2 { + return nil, errors.New("error decoding PIC description text: invalid encoding") + } + desc, err := decodeText(enc, descDataSplit[0]) + if err != nil { + return nil, fmt.Errorf("error decoding PIC description text: %v", err) + } + + var mimeType string + switch ext { + case "jpeg", "jpg": + mimeType = "image/jpeg" + case "png": + mimeType = "image/png" + } + + return &Picture{ + Ext: ext, + MIMEType: mimeType, + Type: pictureTypes[picType], + Description: desc, + Data: descDataSplit[1], + }, nil +} + +// IDv2.{3,4} +// -- Header +//
+// -- readAPICFrame +// Text encoding $xx +// MIME type $00 +// Picture type $xx +// Description $00 (00) +// Picture data +func readAPICFrame(b []byte) (*Picture, error) { + enc := b[0] + mimeDataSplit := bytes.SplitN(b[1:], singleZero, 2) + mimeType := string(mimeDataSplit[0]) + + b = mimeDataSplit[1] + if len(b) < 1 { + return nil, fmt.Errorf("error decoding APIC mimetype") + } + picType := b[0] + + descDataSplit := dataSplit(b[1:], enc) + if len(descDataSplit) != 2 { + return nil, errors.New("error decoding APIC description text: invalid encoding") + } + desc, err := decodeText(enc, descDataSplit[0]) + if err != nil { + return nil, fmt.Errorf("error decoding APIC description text: %v", err) + } + + var ext string + switch mimeType { + case "image/jpeg": + ext = "jpg" + case "image/png": + ext = "png" + } + + return &Picture{ + Ext: ext, + MIMEType: mimeType, + Type: pictureTypes[picType], + Description: desc, + Data: descDataSplit[1], + }, nil +} diff --git a/vendor/github.com/dhowden/tag/id3v2metadata.go b/vendor/github.com/dhowden/tag/id3v2metadata.go new file mode 100644 index 0000000..6185963 --- /dev/null +++ b/vendor/github.com/dhowden/tag/id3v2metadata.go @@ -0,0 +1,141 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "strconv" + "strings" +) + +type frameNames map[string][2]string + +func (f frameNames) Name(s string, fm Format) string { + l, ok := f[s] + if !ok { + return "" + } + + switch fm { + case ID3v2_2: + return l[0] + case ID3v2_3: + return l[1] + case ID3v2_4: + if s == "year" { + return "TDRC" + } + return l[1] + } + return "" +} + +var frames = frameNames(map[string][2]string{ + "title": [2]string{"TT2", "TIT2"}, + "artist": [2]string{"TP1", "TPE1"}, + "album": [2]string{"TAL", "TALB"}, + "album_artist": [2]string{"TP2", "TPE2"}, + "composer": [2]string{"TCM", "TCOM"}, + "year": [2]string{"TYE", "TYER"}, + "track": [2]string{"TRK", "TRCK"}, + "disc": [2]string{"TPA", "TPOS"}, + "genre": [2]string{"TCO", "TCON"}, + "picture": [2]string{"PIC", "APIC"}, + "lyrics": [2]string{"", "USLT"}, + "comment": [2]string{"COM", "COMM"}, +}) + +// metadataID3v2 is the implementation of Metadata used for ID3v2 tags. +type metadataID3v2 struct { + header *id3v2Header + frames map[string]interface{} +} + +func (m metadataID3v2) getString(k string) string { + v, ok := m.frames[k] + if !ok { + return "" + } + return v.(string) +} + +func (m metadataID3v2) Format() Format { return m.header.Version } +func (m metadataID3v2) FileType() FileType { return MP3 } +func (m metadataID3v2) Raw() map[string]interface{} { return m.frames } + +func (m metadataID3v2) Title() string { + return m.getString(frames.Name("title", m.Format())) +} + +func (m metadataID3v2) Artist() string { + return m.getString(frames.Name("artist", m.Format())) +} + +func (m metadataID3v2) Album() string { + return m.getString(frames.Name("album", m.Format())) +} + +func (m metadataID3v2) AlbumArtist() string { + return m.getString(frames.Name("album_artist", m.Format())) +} + +func (m metadataID3v2) Composer() string { + return m.getString(frames.Name("composer", m.Format())) +} + +func (m metadataID3v2) Genre() string { + return id3v2genre(m.getString(frames.Name("genre", m.Format()))) +} + +func (m metadataID3v2) Year() int { + year, _ := strconv.Atoi(m.getString(frames.Name("year", m.Format()))) + return year +} + +func parseXofN(s string) (x, n int) { + xn := strings.Split(s, "/") + if len(xn) != 2 { + x, _ = strconv.Atoi(s) + return x, 0 + } + x, _ = strconv.Atoi(strings.TrimSpace(xn[0])) + n, _ = strconv.Atoi(strings.TrimSpace(xn[1])) + return x, n +} + +func (m metadataID3v2) Track() (int, int) { + return parseXofN(m.getString(frames.Name("track", m.Format()))) +} + +func (m metadataID3v2) Disc() (int, int) { + return parseXofN(m.getString(frames.Name("disc", m.Format()))) +} + +func (m metadataID3v2) Lyrics() string { + t, ok := m.frames[frames.Name("lyrics", m.Format())] + if !ok { + return "" + } + return t.(*Comm).Text +} + +func (m metadataID3v2) Comment() string { + t, ok := m.frames[frames.Name("comment", m.Format())] + if !ok { + return "" + } + // id3v23 has Text, id3v24 has Description + if t.(*Comm).Description == "" { + return trimString(t.(*Comm).Text) + } + return trimString(t.(*Comm).Description) +} + +func (m metadataID3v2) Picture() *Picture { + v, ok := m.frames[frames.Name("picture", m.Format())] + if !ok { + return nil + } + return v.(*Picture) +} diff --git a/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt b/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt new file mode 100644 index 0000000..f7e01d8 --- /dev/null +++ b/vendor/github.com/dhowden/tag/internal/id3v1_test/NOTICE.txt @@ -0,0 +1,3 @@ +All files in this directory are subject to the CC0 1.0 Universal (CC0 1.0) +Public Domain Dedication license. Its contents can be found at: +http://creativecommons.org/publicdomain/zero/1.0 diff --git a/vendor/github.com/dhowden/tag/mp4.go b/vendor/github.com/dhowden/tag/mp4.go new file mode 100644 index 0000000..4c3dd59 --- /dev/null +++ b/vendor/github.com/dhowden/tag/mp4.go @@ -0,0 +1,372 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +var atomTypes = map[int]string{ + 0: "implicit", // automatic based on atom name + 1: "text", + 13: "jpeg", + 14: "png", + 21: "uint8", +} + +// NB: atoms does not include "----", this is handled separately +var atoms = atomNames(map[string]string{ + "\xa9alb": "album", + "\xa9art": "artist", + "\xa9ART": "artist", + "aART": "album_artist", + "\xa9day": "year", + "\xa9nam": "title", + "\xa9gen": "genre", + "trkn": "track", + "\xa9wrt": "composer", + "\xa9too": "encoder", + "cprt": "copyright", + "covr": "picture", + "\xa9grp": "grouping", + "keyw": "keyword", + "\xa9lyr": "lyrics", + "\xa9cmt": "comment", + "tmpo": "tempo", + "cpil": "compilation", + "disk": "disc", +}) + +// Detect PNG image if "implicit" class is used +var pngHeader = []byte{137, 80, 78, 71, 13, 10, 26, 10} + +type atomNames map[string]string + +func (f atomNames) Name(n string) []string { + res := make([]string, 1) + for k, v := range f { + if v == n { + res = append(res, k) + } + } + return res +} + +// metadataMP4 is the implementation of Metadata for MP4 tag (atom) data. +type metadataMP4 struct { + fileType FileType + data map[string]interface{} +} + +// ReadAtoms reads MP4 metadata atoms from the io.ReadSeeker into a Metadata, returning +// non-nil error if there was a problem. +func ReadAtoms(r io.ReadSeeker) (Metadata, error) { + m := metadataMP4{ + data: make(map[string]interface{}), + fileType: UnknownFileType, + } + err := m.readAtoms(r) + return m, err +} + +func (m metadataMP4) readAtoms(r io.ReadSeeker) error { + for { + name, size, err := readAtomHeader(r) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + + switch name { + case "meta": + // next_item_id (int32) + _, err := readBytes(r, 4) + if err != nil { + return err + } + fallthrough + + case "moov", "udta", "ilst": + return m.readAtoms(r) + } + + _, ok := atoms[name] + var data []string + if name == "----" { + name, data, err = readCustomAtom(r, size) + if err != nil { + return err + } + + if name != "----" { + ok = true + size = 0 // already read data + } + } + + if !ok { + _, err := r.Seek(int64(size-8), io.SeekCurrent) + if err != nil { + return err + } + continue + } + + err = m.readAtomData(r, name, size-8, data) + if err != nil { + return err + } + } +} + +func (m metadataMP4) readAtomData(r io.ReadSeeker, name string, size uint32, processedData []string) error { + var b []byte + var err error + var contentType string + if len(processedData) > 0 { + b = []byte(strings.Join(processedData, ";")) // add delimiter if multiple data fields + contentType = "text" + } else { + // read the data + b, err = readBytes(r, int(size)) + if err != nil { + return err + } + if len(b) < 8 { + return fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 8, len(b)) + } + + // "data" + size (4 bytes each) + b = b[8:] + + if len(b) < 3 { + return fmt.Errorf("invalid encoding: expected at least %d bytes, for class, got %d", 3, len(b)) + } + class := getInt(b[1:4]) + var ok bool + contentType, ok = atomTypes[class] + if !ok { + return fmt.Errorf("invalid content type: %v (%x) (%x)", class, b[1:4], b) + } + + // 4: atom version (1 byte) + atom flags (3 bytes) + // 4: NULL (usually locale indicator) + if len(b) < 8 { + return fmt.Errorf("invalid encoding: expected at least %d bytes, for atom version and flags, got %d", 8, len(b)) + } + b = b[8:] + } + + if name == "trkn" || name == "disk" { + if len(b) < 6 { + return fmt.Errorf("invalid encoding: expected at least %d bytes, for track and disk numbers, got %d", 6, len(b)) + } + + m.data[name] = int(b[3]) + m.data[name+"_count"] = int(b[5]) + return nil + } + + if contentType == "implicit" { + if name == "covr" { + if bytes.HasPrefix(b, pngHeader) { + contentType = "png" + } + // TODO(dhowden): Detect JPEG formats too (harder). + } + } + + var data interface{} + switch contentType { + case "implicit": + if _, ok := atoms[name]; ok { + return fmt.Errorf("unhandled implicit content type for required atom: %q", name) + } + return nil + + case "text": + data = string(b) + + case "uint8": + if len(b) < 1 { + return fmt.Errorf("invalid encoding: expected at least %d bytes, for integer tag data, got %d", 1, len(b)) + } + data = getInt(b[:1]) + + case "jpeg", "png": + data = &Picture{ + Ext: contentType, + MIMEType: "image/" + contentType, + Data: b, + } + } + m.data[name] = data + + return nil +} + +func readAtomHeader(r io.ReadSeeker) (name string, size uint32, err error) { + err = binary.Read(r, binary.BigEndian, &size) + if err != nil { + return + } + name, err = readString(r, 4) + return +} + +// Generic atom. +// Should have 3 sub atoms : mean, name and data. +// We check that mean is "com.apple.iTunes" and we use the subname as +// the name, and move to the data atom. +// Data atom could have multiple data values, each with a header. +// If anything goes wrong, we jump at the end of the "----" atom. +func readCustomAtom(r io.ReadSeeker, size uint32) (_ string, data []string, _ error) { + subNames := make(map[string]string) + + for size > 8 { + subName, subSize, err := readAtomHeader(r) + if err != nil { + return "", nil, err + } + + // Remove the size of the atom from the size counter + if size >= subSize { + size -= subSize + } else { + return "", nil, errors.New("--- invalid size") + } + + b, err := readBytes(r, int(subSize-8)) + if err != nil { + return "", nil, err + } + + if len(b) < 4 { + return "", nil, fmt.Errorf("invalid encoding: expected at least %d bytes, got %d", 4, len(b)) + } + switch subName { + case "mean", "name": + subNames[subName] = string(b[4:]) + case "data": + data = append(data, string(b[4:])) + } + } + + // there should remain only the header size + if size != 8 { + err := errors.New("---- atom out of bounds") + return "", nil, err + } + + if subNames["mean"] != "com.apple.iTunes" || subNames["name"] == "" || len(data) == 0 { + return "----", nil, nil + } + return subNames["name"], data, nil +} + +func (metadataMP4) Format() Format { return MP4 } +func (m metadataMP4) FileType() FileType { return m.fileType } + +func (m metadataMP4) Raw() map[string]interface{} { return m.data } + +func (m metadataMP4) getString(n []string) string { + for _, k := range n { + if x, ok := m.data[k]; ok { + return x.(string) + } + } + return "" +} + +func (m metadataMP4) getInt(n []string) int { + for _, k := range n { + if x, ok := m.data[k]; ok { + return x.(int) + } + } + return 0 +} + +func (m metadataMP4) Title() string { + return m.getString(atoms.Name("title")) +} + +func (m metadataMP4) Artist() string { + return m.getString(atoms.Name("artist")) +} + +func (m metadataMP4) Album() string { + return m.getString(atoms.Name("album")) +} + +func (m metadataMP4) AlbumArtist() string { + return m.getString(atoms.Name("album_artist")) +} + +func (m metadataMP4) Composer() string { + return m.getString(atoms.Name("composer")) +} + +func (m metadataMP4) Genre() string { + return m.getString(atoms.Name("genre")) +} + +func (m metadataMP4) Year() int { + date := m.getString(atoms.Name("year")) + if len(date) >= 4 { + year, _ := strconv.Atoi(date[:4]) + return year + } + return 0 +} + +func (m metadataMP4) Track() (int, int) { + x := m.getInt([]string{"trkn"}) + if n, ok := m.data["trkn_count"]; ok { + return x, n.(int) + } + return x, 0 +} + +func (m metadataMP4) Disc() (int, int) { + x := m.getInt([]string{"disk"}) + if n, ok := m.data["disk_count"]; ok { + return x, n.(int) + } + return x, 0 +} + +func (m metadataMP4) Lyrics() string { + t, ok := m.data["\xa9lyr"] + if !ok { + return "" + } + return t.(string) +} + +func (m metadataMP4) Comment() string { + t, ok := m.data["\xa9cmt"] + if !ok { + return "" + } + return t.(string) +} + +func (m metadataMP4) Picture() *Picture { + v, ok := m.data["covr"] + if !ok { + return nil + } + p, _ := v.(*Picture) + return p +} diff --git a/vendor/github.com/dhowden/tag/ogg.go b/vendor/github.com/dhowden/tag/ogg.go new file mode 100644 index 0000000..bc26d4a --- /dev/null +++ b/vendor/github.com/dhowden/tag/ogg.go @@ -0,0 +1,119 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "errors" + "io" +) + +const ( + idType int = 1 + commentType int = 3 +) + +// ReadOGGTags reads OGG metadata from the io.ReadSeeker, returning the resulting +// metadata in a Metadata implementation, or non-nil error if there was a problem. +// See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html +// and http://www.xiph.org/ogg/doc/framing.html for details. +func ReadOGGTags(r io.ReadSeeker) (Metadata, error) { + oggs, err := readString(r, 4) + if err != nil { + return nil, err + } + if oggs != "OggS" { + return nil, errors.New("expected 'OggS'") + } + + // Skip 22 bytes of Page header to read page_segments length byte at position 26 + // See http://www.xiph.org/ogg/doc/framing.html + _, err = r.Seek(22, io.SeekCurrent) + if err != nil { + return nil, err + } + + nS, err := readInt(r, 1) + if err != nil { + return nil, err + } + + // Seek and discard the segments + _, err = r.Seek(int64(nS), io.SeekCurrent) + if err != nil { + return nil, err + } + + // First packet type is identification, type 1 + t, err := readInt(r, 1) + if err != nil { + return nil, err + } + if t != idType { + return nil, errors.New("expected 'vorbis' identification type 1") + } + + // Seek and discard 29 bytes from common and identification header + // See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2 + _, err = r.Seek(29, io.SeekCurrent) + if err != nil { + return nil, err + } + + // Beginning of a new page. Comment packet is on a separate page + // See http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-132000A.2 + oggs, err = readString(r, 4) + if err != nil { + return nil, err + } + if oggs != "OggS" { + return nil, errors.New("expected 'OggS'") + } + + // Skip page 2 header, same as line 30 + _, err = r.Seek(22, io.SeekCurrent) + if err != nil { + return nil, err + } + + nS, err = readInt(r, 1) + if err != nil { + return nil, err + } + + _, err = r.Seek(int64(nS), io.SeekCurrent) + if err != nil { + return nil, err + } + + // Packet type is comment, type 3 + t, err = readInt(r, 1) + if err != nil { + return nil, err + } + if t != commentType { + return nil, errors.New("expected 'vorbis' comment type 3") + } + + // Seek and discard 6 bytes from common header + _, err = r.Seek(6, io.SeekCurrent) + if err != nil { + return nil, err + } + + m := &metadataOGG{ + newMetadataVorbis(), + } + + err = m.readVorbisComment(r) + return m, err +} + +type metadataOGG struct { + *metadataVorbis +} + +func (m *metadataOGG) FileType() FileType { + return OGG +} diff --git a/vendor/github.com/dhowden/tag/sum.go b/vendor/github.com/dhowden/tag/sum.go new file mode 100644 index 0000000..97b27a7 --- /dev/null +++ b/vendor/github.com/dhowden/tag/sum.go @@ -0,0 +1,219 @@ +package tag + +import ( + "crypto/sha1" + "encoding/binary" + "errors" + "fmt" + "hash" + "io" +) + +// Sum creates a checksum of the audio file data provided by the io.ReadSeeker which is metadata +// (ID3, MP4) invariant. +func Sum(r io.ReadSeeker) (string, error) { + b, err := readBytes(r, 11) + if err != nil { + return "", err + } + + _, err = r.Seek(-11, io.SeekCurrent) + if err != nil { + return "", fmt.Errorf("could not seek back to original position: %v", err) + } + + switch { + case string(b[0:4]) == "fLaC": + return SumFLAC(r) + + case string(b[4:11]) == "ftypM4A": + return SumAtoms(r) + + case string(b[0:3]) == "ID3": + return SumID3v2(r) + } + + h, err := SumID3v1(r) + if err != nil { + if err == ErrNotID3v1 { + return SumAll(r) + } + return "", err + } + return h, nil +} + +// SumAll returns a checksum of the content from the reader (until EOF). +func SumAll(r io.ReadSeeker) (string, error) { + h := sha1.New() + _, err := io.Copy(h, r) + if err != nil { + return "", nil + } + return hashSum(h), nil +} + +// SumAtoms constructs a checksum of MP4 audio file data provided by the io.ReadSeeker which is +// metadata invariant. +func SumAtoms(r io.ReadSeeker) (string, error) { + for { + var size uint32 + err := binary.Read(r, binary.BigEndian, &size) + if err != nil { + if err == io.EOF { + return "", fmt.Errorf("reached EOF before audio data") + } + return "", err + } + + name, err := readString(r, 4) + if err != nil { + return "", err + } + + switch name { + case "meta": + // next_item_id (int32) + _, err := r.Seek(4, io.SeekCurrent) + if err != nil { + return "", err + } + fallthrough + + case "moov", "udta", "ilst": + continue + + case "mdat": // stop when we get to the data + h := sha1.New() + _, err := io.CopyN(h, r, int64(size-8)) + if err != nil { + return "", fmt.Errorf("error reading audio data: %v", err) + } + return hashSum(h), nil + } + + _, err = r.Seek(int64(size-8), io.SeekCurrent) + if err != nil { + return "", fmt.Errorf("error reading '%v' tag: %v", name, err) + } + } +} + +func sizeToEndOffset(r io.ReadSeeker, offset int64) (int64, error) { + n, err := r.Seek(-128, io.SeekEnd) + if err != nil { + return 0, fmt.Errorf("error seeking end offset (%d bytes): %v", offset, err) + } + + _, err = r.Seek(-n, io.SeekCurrent) + if err != nil { + return 0, fmt.Errorf("error seeking back to original position: %v", err) + } + return n, nil +} + +// SumID3v1 constructs a checksum of MP3 audio file data (assumed to have ID3v1 tags) provided +// by the io.ReadSeeker which is metadata invariant. +func SumID3v1(r io.ReadSeeker) (string, error) { + n, err := sizeToEndOffset(r, 128) + if err != nil { + return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err) + } + + // TODO: improve this check??? + if n <= 0 { + return "", fmt.Errorf("file size must be greater than 128 bytes (ID3v1 header size) for MP3") + } + + h := sha1.New() + _, err = io.CopyN(h, r, n) + if err != nil { + return "", fmt.Errorf("error reading %v bytes: %v", n, err) + } + return hashSum(h), nil +} + +// SumID3v2 constructs a checksum of MP3 audio file data (assumed to have ID3v2 tags) provided by the +// io.ReadSeeker which is metadata invariant. +func SumID3v2(r io.ReadSeeker) (string, error) { + header, _, err := readID3v2Header(r) + if err != nil { + return "", fmt.Errorf("error reading ID3v2 header: %v", err) + } + + _, err = r.Seek(int64(header.Size), io.SeekCurrent) + if err != nil { + return "", fmt.Errorf("error seeking to end of ID3V2 header: %v", err) + } + + n, err := sizeToEndOffset(r, 128) + if err != nil { + return "", fmt.Errorf("error determining read size to ID3v1 header: %v", err) + } + + // TODO: remove this check????? + if n < 0 { + return "", fmt.Errorf("file size must be greater than 128 bytes for MP3: %v bytes", n) + } + + h := sha1.New() + _, err = io.CopyN(h, r, n) + if err != nil { + return "", fmt.Errorf("error reading %v bytes: %v", n, err) + } + return hashSum(h), nil +} + +// SumFLAC costructs a checksum of the FLAC audio file data provided by the io.ReadSeeker (ignores +// metadata fields). +func SumFLAC(r io.ReadSeeker) (string, error) { + flac, err := readString(r, 4) + if err != nil { + return "", err + } + if flac != "fLaC" { + return "", errors.New("expected 'fLaC'") + } + + for { + last, err := skipFLACMetadataBlock(r) + if err != nil { + return "", err + } + + if last { + break + } + } + + h := sha1.New() + _, err = io.Copy(h, r) + if err != nil { + return "", fmt.Errorf("error reading data bytes from FLAC: %v", err) + } + return hashSum(h), nil +} + +func skipFLACMetadataBlock(r io.ReadSeeker) (last bool, err error) { + blockHeader, err := readBytes(r, 1) + if err != nil { + return + } + + if getBit(blockHeader[0], 7) { + blockHeader[0] ^= (1 << 7) + last = true + } + + blockLen, err := readInt(r, 3) + if err != nil { + return + } + + _, err = r.Seek(int64(blockLen), io.SeekCurrent) + return +} + +func hashSum(h hash.Hash) string { + return fmt.Sprintf("%x", h.Sum([]byte{})) +} diff --git a/vendor/github.com/dhowden/tag/tag.go b/vendor/github.com/dhowden/tag/tag.go new file mode 100644 index 0000000..306f1d7 --- /dev/null +++ b/vendor/github.com/dhowden/tag/tag.go @@ -0,0 +1,147 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package tag provides MP3 (ID3: v1, 2.2, 2.3 and 2.4), MP4, FLAC and OGG metadata detection, +// parsing and artwork extraction. +// +// Detect and parse tag metadata from an io.ReadSeeker (i.e. an *os.File): +// m, err := tag.ReadFrom(f) +// if err != nil { +// log.Fatal(err) +// } +// log.Print(m.Format()) // The detected format. +// log.Print(m.Title()) // The title of the track (see Metadata interface for more details). +package tag + +import ( + "errors" + "fmt" + "io" +) + +// ErrNoTagsFound is the error returned by ReadFrom when the metadata format +// cannot be identified. +var ErrNoTagsFound = errors.New("no tags found") + +// ReadFrom detects and parses audio file metadata tags (currently supports ID3v1,2.{2,3,4}, MP4, FLAC/OGG). +// Returns non-nil error if the format of the given data could not be determined, or if there was a problem +// parsing the data. +func ReadFrom(r io.ReadSeeker) (Metadata, error) { + b, err := readBytes(r, 11) + if err != nil { + return nil, err + } + + _, err = r.Seek(-11, io.SeekCurrent) + if err != nil { + return nil, fmt.Errorf("could not seek back to original position: %v", err) + } + + switch { + case string(b[0:4]) == "fLaC": + return ReadFLACTags(r) + + case string(b[0:4]) == "OggS": + return ReadOGGTags(r) + + case string(b[4:8]) == "ftyp": + return ReadAtoms(r) + + case string(b[0:3]) == "ID3": + return ReadID3v2Tags(r) + + case string(b[0:4]) == "DSD ": + return ReadDSFTags(r) + } + + m, err := ReadID3v1Tags(r) + if err != nil { + if err == ErrNotID3v1 { + err = ErrNoTagsFound + } + return nil, err + } + return m, nil +} + +// Format is an enumeration of metadata types supported by this package. +type Format string + +// Supported tag formats. +const ( + UnknownFormat Format = "" // Unknown Format. + ID3v1 Format = "ID3v1" // ID3v1 tag format. + ID3v2_2 Format = "ID3v2.2" // ID3v2.2 tag format. + ID3v2_3 Format = "ID3v2.3" // ID3v2.3 tag format (most common). + ID3v2_4 Format = "ID3v2.4" // ID3v2.4 tag format. + MP4 Format = "MP4" // MP4 tag (atom) format (see http://www.ftyps.com/ for a full file type list) + VORBIS Format = "VORBIS" // Vorbis Comment tag format. +) + +// FileType is an enumeration of the audio file types supported by this package, in particular +// there are audio file types which share metadata formats, and this type is used to distinguish +// between them. +type FileType string + +// Supported file types. +const ( + UnknownFileType FileType = "" // Unknown FileType. + MP3 FileType = "MP3" // MP3 file + M4A FileType = "M4A" // M4A file Apple iTunes (ACC) Audio + M4B FileType = "M4B" // M4A file Apple iTunes (ACC) Audio Book + M4P FileType = "M4P" // M4A file Apple iTunes (ACC) AES Protected Audio + ALAC FileType = "ALAC" // Apple Lossless file FIXME: actually detect this + FLAC FileType = "FLAC" // FLAC file + OGG FileType = "OGG" // OGG file + DSF FileType = "DSF" // DSF file DSD Sony format see https://dsd-guide.com/sites/default/files/white-papers/DSFFileFormatSpec_E.pdf +) + +// Metadata is an interface which is used to describe metadata retrieved by this package. +type Metadata interface { + // Format returns the metadata Format used to encode the data. + Format() Format + + // FileType returns the file type of the audio file. + FileType() FileType + + // Title returns the title of the track. + Title() string + + // Album returns the album name of the track. + Album() string + + // Artist returns the artist name of the track. + Artist() string + + // AlbumArtist returns the album artist name of the track. + AlbumArtist() string + + // Composer returns the composer of the track. + Composer() string + + // Year returns the year of the track. + Year() int + + // Genre returns the genre of the track. + Genre() string + + // Track returns the track number and total tracks, or zero values if unavailable. + Track() (int, int) + + // Disc returns the disc number and total discs, or zero values if unavailable. + Disc() (int, int) + + // Picture returns a picture, or nil if not available. + Picture() *Picture + + // Lyrics returns the lyrics, or an empty string if unavailable. + Lyrics() string + + // Comment returns the comment, or an empty string if unavailable. + Comment() string + + // Raw returns the raw mapping of retrieved tag names and associated values. + // NB: tag/atom names are not standardised between formats. + Raw() map[string]interface{} +} diff --git a/vendor/github.com/dhowden/tag/util.go b/vendor/github.com/dhowden/tag/util.go new file mode 100644 index 0000000..ff9c4f1 --- /dev/null +++ b/vendor/github.com/dhowden/tag/util.go @@ -0,0 +1,81 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "encoding/binary" + "io" +) + +func getBit(b byte, n uint) bool { + x := byte(1 << n) + return (b & x) == x +} + +func get7BitChunkedInt(b []byte) int { + var n int + for _, x := range b { + n = n << 7 + n |= int(x) + } + return n +} + +func getInt(b []byte) int { + var n int + for _, x := range b { + n = n << 8 + n |= int(x) + } + return n +} + +func getIntLittleEndian(b []byte) int { + var n int + for i := len(b) - 1; i >= 0; i-- { + n = n << 8 + n |= int(b[i]) + } + return n +} + +func readBytes(r io.Reader, n int) ([]byte, error) { + b := make([]byte, n) + _, err := io.ReadFull(r, b) + if err != nil { + return nil, err + } + return b, nil +} + +func readString(r io.Reader, n int) (string, error) { + b, err := readBytes(r, n) + if err != nil { + return "", err + } + return string(b), nil +} + +func readInt(r io.Reader, n int) (int, error) { + b, err := readBytes(r, n) + if err != nil { + return 0, err + } + return getInt(b), nil +} + +func read7BitChunkedInt(r io.Reader, n int) (int, error) { + b, err := readBytes(r, n) + if err != nil { + return 0, err + } + return get7BitChunkedInt(b), nil +} + +func readInt32LittleEndian(r io.Reader) (int, error) { + var n int32 + err := binary.Read(r, binary.LittleEndian, &n) + return int(n), err +} diff --git a/vendor/github.com/dhowden/tag/vorbis.go b/vendor/github.com/dhowden/tag/vorbis.go new file mode 100644 index 0000000..9f5ecb8 --- /dev/null +++ b/vendor/github.com/dhowden/tag/vorbis.go @@ -0,0 +1,255 @@ +// Copyright 2015, David Howden +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package tag + +import ( + "errors" + "fmt" + "io" + "strconv" + "strings" + "time" +) + +func newMetadataVorbis() *metadataVorbis { + return &metadataVorbis{ + c: make(map[string]string), + } +} + +type metadataVorbis struct { + c map[string]string // the vorbis comments + p *Picture +} + +func (m *metadataVorbis) readVorbisComment(r io.Reader) error { + vendorLen, err := readInt32LittleEndian(r) + if err != nil { + return err + } + + if vendorLen < 0 { + return fmt.Errorf("invalid encoding: expected positive length, got %d", vendorLen) + } + + vendor, err := readString(r, vendorLen) + if err != nil { + return err + } + m.c["vendor"] = vendor + + commentsLen, err := readInt32LittleEndian(r) + if err != nil { + return err + } + + for i := 0; i < commentsLen; i++ { + l, err := readInt32LittleEndian(r) + if err != nil { + return err + } + s, err := readString(r, l) + if err != nil { + return err + } + k, v, err := parseComment(s) + if err != nil { + return err + } + m.c[strings.ToLower(k)] = v + } + return nil +} + +func (m *metadataVorbis) readPictureBlock(r io.Reader) error { + b, err := readInt(r, 4) + if err != nil { + return err + } + pictureType, ok := pictureTypes[byte(b)] + if !ok { + return fmt.Errorf("invalid picture type: %v", b) + } + mimeLen, err := readInt(r, 4) + if err != nil { + return err + } + mime, err := readString(r, mimeLen) + if err != nil { + return err + } + + ext := "" + switch mime { + case "image/jpeg": + ext = "jpg" + case "image/png": + ext = "png" + case "image/gif": + ext = "gif" + } + + descLen, err := readInt(r, 4) + if err != nil { + return err + } + desc, err := readString(r, descLen) + if err != nil { + return err + } + + // We skip width <32>, height <32>, colorDepth <32>, coloresUsed <32> + _, err = readInt(r, 4) // width + if err != nil { + return err + } + _, err = readInt(r, 4) // height + if err != nil { + return err + } + _, err = readInt(r, 4) // color depth + if err != nil { + return err + } + _, err = readInt(r, 4) // colors used + if err != nil { + return err + } + + dataLen, err := readInt(r, 4) + if err != nil { + return err + } + data := make([]byte, dataLen) + _, err = io.ReadFull(r, data) + if err != nil { + return err + } + + m.p = &Picture{ + Ext: ext, + MIMEType: mime, + Type: pictureType, + Description: desc, + Data: data, + } + return nil +} + +func parseComment(c string) (k, v string, err error) { + kv := strings.SplitN(c, "=", 2) + if len(kv) != 2 { + err = errors.New("vorbis comment must contain '='") + return + } + k = kv[0] + v = kv[1] + return +} + +func (m *metadataVorbis) Format() Format { + return VORBIS +} + +func (m *metadataVorbis) Raw() map[string]interface{} { + raw := make(map[string]interface{}, len(m.c)) + for k, v := range m.c { + raw[k] = v + } + return raw +} + +func (m *metadataVorbis) Title() string { + return m.c["title"] +} + +func (m *metadataVorbis) Artist() string { + // PERFORMER + // The artist(s) who performed the work. In classical music this would be the + // conductor, orchestra, soloists. In an audio book it would be the actor who + // did the reading. In popular music this is typically the same as the ARTIST + // and is omitted. + if m.c["performer"] != "" { + return m.c["performer"] + } + return m.c["artist"] +} + +func (m *metadataVorbis) Album() string { + return m.c["album"] +} + +func (m *metadataVorbis) AlbumArtist() string { + // This field isn't actually included in the standard, though + // it is commonly assigned to albumartist. + return m.c["albumartist"] +} + +func (m *metadataVorbis) Composer() string { + // ARTIST + // The artist generally considered responsible for the work. In popular music + // this is usually the performing band or singer. For classical music it would + // be the composer. For an audio book it would be the author of the original text. + if m.c["composer"] != "" { + return m.c["composer"] + } + if m.c["performer"] == "" { + return "" + } + return m.c["artist"] +} + +func (m *metadataVorbis) Genre() string { + return m.c["genre"] +} + +func (m *metadataVorbis) Year() int { + var dateFormat string + + // The date need to follow the international standard https://en.wikipedia.org/wiki/ISO_8601 + // and obviously the VorbisComment standard https://wiki.xiph.org/VorbisComment#Date_and_time + switch len(m.c["date"]) { + case 0: + return 0 + case 4: + dateFormat = "2006" + case 7: + dateFormat = "2006-01" + case 10: + dateFormat = "2006-01-02" + } + + t, _ := time.Parse(dateFormat, m.c["date"]) + return t.Year() +} + +func (m *metadataVorbis) Track() (int, int) { + x, _ := strconv.Atoi(m.c["tracknumber"]) + // https://wiki.xiph.org/Field_names + n, _ := strconv.Atoi(m.c["tracktotal"]) + return x, n +} + +func (m *metadataVorbis) Disc() (int, int) { + // https://wiki.xiph.org/Field_names + x, _ := strconv.Atoi(m.c["discnumber"]) + n, _ := strconv.Atoi(m.c["disctotal"]) + return x, n +} + +func (m *metadataVorbis) Lyrics() string { + return m.c["lyrics"] +} + +func (m *metadataVorbis) Comment() string { + if m.c["comment"] != "" { + return m.c["comment"] + } + return m.c["description"] +} + +func (m *metadataVorbis) Picture() *Picture { + return m.p +} diff --git a/vendor/github.com/gorilla/mux/.travis.yml b/vendor/github.com/gorilla/mux/.travis.yml new file mode 100644 index 0000000..d003ad9 --- /dev/null +++ b/vendor/github.com/gorilla/mux/.travis.yml @@ -0,0 +1,24 @@ +language: go + + +matrix: + include: + - go: 1.7.x + - go: 1.8.x + - go: 1.9.x + - go: 1.10.x + - go: 1.11.x + - go: 1.x + env: LATEST=true + - go: tip + allow_failures: + - go: tip + +install: + - # Skip + +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d .) + - if [[ "$LATEST" = true ]]; then go vet .; fi + - go test -v -race ./... diff --git a/vendor/github.com/gorilla/mux/AUTHORS b/vendor/github.com/gorilla/mux/AUTHORS new file mode 100644 index 0000000..b722392 --- /dev/null +++ b/vendor/github.com/gorilla/mux/AUTHORS @@ -0,0 +1,8 @@ +# This is the official list of gorilla/mux authors for copyright purposes. +# +# Please keep the list sorted. + +Google LLC (https://opensource.google.com/) +Kamil Kisielk +Matt Silverlock +Rodrigo Moraes (https://github.com/moraes) diff --git a/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..232be82 --- /dev/null +++ b/vendor/github.com/gorilla/mux/ISSUE_TEMPLATE.md @@ -0,0 +1,11 @@ +**What version of Go are you running?** (Paste the output of `go version`) + + +**What version of gorilla/mux are you at?** (Paste the output of `git rev-parse HEAD` inside `$GOPATH/src/github.com/gorilla/mux`) + + +**Describe your problem** (and what you have tried so far) + + +**Paste a minimal, runnable, reproduction of your issue below** (use backticks to format it) + diff --git a/vendor/github.com/gorilla/mux/LICENSE b/vendor/github.com/gorilla/mux/LICENSE new file mode 100644 index 0000000..6903df6 --- /dev/null +++ b/vendor/github.com/gorilla/mux/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2012-2018 The Gorilla Authors. 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. + * Neither the name of Google Inc. nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +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 +OWNER 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/gorilla/mux/README.md b/vendor/github.com/gorilla/mux/README.md new file mode 100644 index 0000000..c661599 --- /dev/null +++ b/vendor/github.com/gorilla/mux/README.md @@ -0,0 +1,649 @@ +# gorilla/mux + +[![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) +[![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) + +![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) + +https://www.gorillatoolkit.org/pkg/mux + +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. + +The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: + +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. +* Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. +* URL hosts, paths and query values can have variables with an optional regular expression. +* Registered URLs can be built, or "reversed", which helps maintaining references to resources. +* Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Registered URLs](#registered-urls) +* [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) +* [Testing Handlers](#testing-handlers) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples + +Let's start registering a couple of URL paths and handlers: + +```go +func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) +} +``` + +Here we register three routes mapping URL paths to handlers. This is equivalent to how `http.HandleFunc()` works: if an incoming request URL matches one of the paths, the corresponding handler is called passing (`http.ResponseWriter`, `*http.Request`) as parameters. + +Paths can have variables. They are defined using the format `{name}` or `{name:pattern}`. If a regular expression pattern is not defined, the matched variable will be anything until the next slash. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/products/{key}", ProductHandler) +r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: + +```go +func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) +} +``` + +And this is all you need to know about the basic usage. More advanced options are explained below. + +### Matching Routes + +Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: + +```go +r := mux.NewRouter() +// Only matches if domain is "www.example.com". +r.Host("www.example.com") +// Matches a dynamic subdomain. +r.Host("{subdomain:[a-z]+}.example.com") +``` + +There are several other matchers that can be added. To match path prefixes: + +```go +r.PathPrefix("/products/") +``` + +...or HTTP methods: + +```go +r.Methods("GET", "POST") +``` + +...or URL schemes: + +```go +r.Schemes("https") +``` + +...or header values: + +```go +r.Headers("X-Requested-With", "XMLHttpRequest") +``` + +...or query values: + +```go +r.Queries("key", "value") +``` + +...or to use a custom matcher function: + +```go +r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 +}) +``` + +...and finally, it is possible to combine several matchers in a single route: + +```go +r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") +``` + +Routes are tested in the order they were added to the router. If two routes match, the first one wins: + +```go +r := mux.NewRouter() +r.HandleFunc("/specific", specificHandler) +r.PathPrefix("/").Handler(catchAllHandler) +``` + +Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". + +For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: + +```go +r := mux.NewRouter() +s := r.Host("www.example.com").Subrouter() +``` + +Then register routes in the subrouter: + +```go +s.HandleFunc("/products/", ProductsHandler) +s.HandleFunc("/products/{key}", ProductHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +``` + +The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define subrouters in a central place and then parts of the app can register its paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, the inner routes use it as base for their paths: + +```go +r := mux.NewRouter() +s := r.PathPrefix("/products").Subrouter() +// "/products/" +s.HandleFunc("/", ProductsHandler) +// "/products/{key}/" +s.HandleFunc("/{key}/", ProductHandler) +// "/products/{key}/details" +s.HandleFunc("/{key}/details", ProductDetailsHandler) +``` + + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/\*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: + +```go +r := mux.NewRouter() +r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") +``` + +To build a URL, get the route and call the `URL()` method, passing a sequence of key/value pairs for the route variables. For the previous route, we would do: + +```go +url, err := r.Get("article").URL("category", "technology", "id", "42") +``` + +...and the result will be a `url.URL` with the following path: + +``` +"/articles/technology/42" +``` + +This also works for host and query value variables: + +```go +r := mux.NewRouter() +r.Host("{subdomain}.example.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + +// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") +``` + +All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + +```go +r.HeadersRegexp("Content-Type", "application/(text|json)") +``` + +...and the route will match both requests with a Content-Type of `application/json` as well as `application/text` + +There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: + +```go +// "http://news.example.com/" +host, err := r.Get("article").URLHost("subdomain", "news") + +// "/articles/technology/42" +path, err := r.Get("article").URLPath("category", "technology", "id", "42") +``` + +And if you use subrouters, host and path defined separately can be built as well: + +```go +r := mux.NewRouter() +s := r.Host("{subdomain}.example.com").Subrouter() +s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + +// "http://news.example.com/articles/technology/42" +url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") +``` + +### Walking Routes + +The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, +the following prints all of the registered routes: + +```go +package main + +import ( + "fmt" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +func handler(w http.ResponseWriter, r *http.Request) { + return +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} +``` + +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.Use()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.Use(loggingMiddleware) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{} +amw.Populate() + +r.Use(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + // Need to create a router that we can pass the request through so that the vars will be added to the context + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !tc.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + tc.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) + + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) +} +``` + +## License + +BSD licensed. See the LICENSE file for details. diff --git a/vendor/github.com/gorilla/mux/context.go b/vendor/github.com/gorilla/mux/context.go new file mode 100644 index 0000000..665940a --- /dev/null +++ b/vendor/github.com/gorilla/mux/context.go @@ -0,0 +1,18 @@ +package mux + +import ( + "context" + "net/http" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return r.Context().Value(key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + return r.WithContext(context.WithValue(r.Context(), key, val)) +} diff --git a/vendor/github.com/gorilla/mux/doc.go b/vendor/github.com/gorilla/mux/doc.go new file mode 100644 index 0000000..38957de --- /dev/null +++ b/vendor/github.com/gorilla/mux/doc.go @@ -0,0 +1,306 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package mux implements a request router and dispatcher. + +The name mux stands for "HTTP request multiplexer". Like the standard +http.ServeMux, mux.Router matches incoming requests against a list of +registered routes and calls a handler for the route that matches the URL +or other conditions. The main features are: + + * Requests can be matched based on URL host, path, path prefix, schemes, + header and query values, HTTP methods or using custom matchers. + * URL hosts, paths and query values can have variables with an optional + regular expression. + * Registered URLs can be built, or "reversed", which helps maintaining + references to resources. + * Routes can be used as subrouters: nested routes are only tested if the + parent route matches. This is useful to define groups of routes that + share common conditions like a host, a path prefix or other repeated + attributes. As a bonus, this optimizes request matching. + * It implements the http.Handler interface so it is compatible with the + standard http.ServeMux. + +Let's start registering a couple of URL paths and handlers: + + func main() { + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) + } + +Here we register three routes mapping URL paths to handlers. This is +equivalent to how http.HandleFunc() works: if an incoming request URL matches +one of the paths, the corresponding handler is called passing +(http.ResponseWriter, *http.Request) as parameters. + +Paths can have variables. They are defined using the format {name} or +{name:pattern}. If a regular expression pattern is not defined, the matched +variable will be anything until the next slash. For example: + + r := mux.NewRouter() + r.HandleFunc("/products/{key}", ProductHandler) + r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) + +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + +The names are used to create a map of route variables which can be retrieved +calling mux.Vars(): + + vars := mux.Vars(request) + category := vars["category"] + +Note that if any capturing groups are present, mux will panic() during parsing. To prevent +this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to +"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably +when capturing groups were present. + +And this is all you need to know about the basic usage. More advanced options +are explained below. + +Routes can also be restricted to a domain or subdomain. Just define a host +pattern to be matched. They can also have variables: + + r := mux.NewRouter() + // Only matches if domain is "www.example.com". + r.Host("www.example.com") + // Matches a dynamic subdomain. + r.Host("{subdomain:[a-z]+}.domain.com") + +There are several other matchers that can be added. To match path prefixes: + + r.PathPrefix("/products/") + +...or HTTP methods: + + r.Methods("GET", "POST") + +...or URL schemes: + + r.Schemes("https") + +...or header values: + + r.Headers("X-Requested-With", "XMLHttpRequest") + +...or query values: + + r.Queries("key", "value") + +...or to use a custom matcher function: + + r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { + return r.ProtoMajor == 0 + }) + +...and finally, it is possible to combine several matchers in a single route: + + r.HandleFunc("/products", ProductsHandler). + Host("www.example.com"). + Methods("GET"). + Schemes("http") + +Setting the same matching conditions again and again can be boring, so we have +a way to group several routes that share the same requirements. +We call it "subrouting". + +For example, let's say we have several URLs that should only match when the +host is "www.example.com". Create a route for that host and get a "subrouter" +from it: + + r := mux.NewRouter() + s := r.Host("www.example.com").Subrouter() + +Then register routes in the subrouter: + + s.HandleFunc("/products/", ProductsHandler) + s.HandleFunc("/products/{key}", ProductHandler) + s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) + +The three URL paths we registered above will only be tested if the domain is +"www.example.com", because the subrouter is tested first. This is not +only convenient, but also optimizes request matching. You can create +subrouters combining any attribute matchers accepted by a route. + +Subrouters can be used to create domain or path "namespaces": you define +subrouters in a central place and then parts of the app can register its +paths relatively to a given subrouter. + +There's one more thing about subroutes. When a subrouter has a path prefix, +the inner routes use it as base for their paths: + + r := mux.NewRouter() + s := r.PathPrefix("/products").Subrouter() + // "/products/" + s.HandleFunc("/", ProductsHandler) + // "/products/{key}/" + s.HandleFunc("/{key}/", ProductHandler) + // "/products/{key}/details" + s.HandleFunc("/{key}/details", ProductDetailsHandler) + +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + +Now let's see how to build registered URLs. + +Routes can be named. All routes that define a name can have their URLs built, +or "reversed". We define a name calling Name() on a route. For example: + + r := mux.NewRouter() + r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). + Name("article") + +To build a URL, get the route and call the URL() method, passing a sequence of +key/value pairs for the route variables. For the previous route, we would do: + + url, err := r.Get("article").URL("category", "technology", "id", "42") + +...and the result will be a url.URL with the following path: + + "/articles/technology/42" + +This also works for host and query value variables: + + r := mux.NewRouter() + r.Host("{subdomain}.domain.com"). + Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). + HandlerFunc(ArticleHandler). + Name("article") + + // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42", + "filter", "gorilla") + +All variables defined in the route are required, and their values must +conform to the corresponding patterns. These requirements guarantee that a +generated URL will always match a registered route -- the only exception is +for explicitly defined "build-only" routes which never match. + +Regex support also exists for matching Headers within a route. For example, we could do: + + r.HeadersRegexp("Content-Type", "application/(text|json)") + +...and the route will match both requests with a Content-Type of `application/json` as well as +`application/text` + +There's also a way to build only the URL host or path for a route: +use the methods URLHost() or URLPath() instead. For the previous route, +we would do: + + // "http://news.domain.com/" + host, err := r.Get("article").URLHost("subdomain", "news") + + // "/articles/technology/42" + path, err := r.Get("article").URLPath("category", "technology", "id", "42") + +And if you use subrouters, host and path defined separately can be built +as well: + + r := mux.NewRouter() + s := r.Host("{subdomain}.domain.com").Subrouter() + s.Path("/articles/{category}/{id:[0-9]+}"). + HandlerFunc(ArticleHandler). + Name("article") + + // "http://news.domain.com/articles/technology/42" + url, err := r.Get("article").URL("subdomain", "news", + "category", "technology", + "id", "42") + +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.Use(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. + +*/ +package mux diff --git a/vendor/github.com/gorilla/mux/go.mod b/vendor/github.com/gorilla/mux/go.mod new file mode 100644 index 0000000..cfc8ede --- /dev/null +++ b/vendor/github.com/gorilla/mux/go.mod @@ -0,0 +1 @@ +module github.com/gorilla/mux diff --git a/vendor/github.com/gorilla/mux/middleware.go b/vendor/github.com/gorilla/mux/middleware.go new file mode 100644 index 0000000..ceb812c --- /dev/null +++ b/vendor/github.com/gorilla/mux/middleware.go @@ -0,0 +1,72 @@ +package mux + +import ( + "net/http" + "strings" +) + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// Middleware allows MiddlewareFunc to implement the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} + +// CORSMethodMiddleware sets the Access-Control-Allow-Methods response header +// on a request, by matching routes based only on paths. It also handles +// OPTIONS requests, by settings Access-Control-Allow-Methods, and then +// returning without calling the next http handler. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var allMethods []string + + err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { + for _, m := range route.matchers { + if _, ok := m.(*routeRegexp); ok { + if m.Match(req, &RouteMatch{}) { + methods, err := route.GetMethods() + if err != nil { + return err + } + + allMethods = append(allMethods, methods...) + } + break + } + } + return nil + }) + + if err == nil { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(append(allMethods, "OPTIONS"), ",")) + + if req.Method == "OPTIONS" { + return + } + } + + next.ServeHTTP(w, req) + }) + } +} diff --git a/vendor/github.com/gorilla/mux/mux.go b/vendor/github.com/gorilla/mux/mux.go new file mode 100644 index 0000000..a2cd193 --- /dev/null +++ b/vendor/github.com/gorilla/mux/mux.go @@ -0,0 +1,607 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "path" + "regexp" +) + +var ( + // ErrMethodMismatch is returned when the method in the request does not match + // the method defined against the route. + ErrMethodMismatch = errors.New("method is not allowed") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") +) + +// NewRouter returns a new router instance. +func NewRouter() *Router { + return &Router{namedRoutes: make(map[string]*Route)} +} + +// Router registers routes to be matched and dispatches a handler. +// +// It implements the http.Handler interface, so it can be registered to serve +// requests: +// +// var router = mux.NewRouter() +// +// func main() { +// http.Handle("/", router) +// } +// +// Or, for Google App Engine, register it in a init() function: +// +// func init() { +// http.Handle("/", router) +// } +// +// This will send all incoming requests to the router. +type Router struct { + // Configurable Handler to be used when no route matches. + NotFoundHandler http.Handler + + // Configurable Handler to be used when the request method does not match the route. + MethodNotAllowedHandler http.Handler + + // Routes to be matched, in order. + routes []*Route + + // Routes by name for URL building. + namedRoutes map[string]*Route + + // If true, do not clear the request context after handling the request. + // + // Deprecated: No effect when go1.7+ is used, since the context is stored + // on the request itself. + KeepContext bool + + // Slice of middlewares to be called after a match is found + middlewares []middleware + + // configuration shared with `Route` + routeConf +} + +// common route configuration shared between `Router` and `Route` +type routeConf struct { + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + + // Manager for the variables from host and path. + regexp routeRegexpGroup + + // List of matchers. + matchers []matcher + + // The scheme used when building URLs. + buildScheme string + + buildVarsFunc BuildVarsFunc +} + +// returns an effective deep copy of `routeConf` +func copyRouteConf(r routeConf) routeConf { + c := r + + if r.regexp.path != nil { + c.regexp.path = copyRouteRegexp(r.regexp.path) + } + + if r.regexp.host != nil { + c.regexp.host = copyRouteRegexp(r.regexp.host) + } + + c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) + for _, q := range r.regexp.queries { + c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) + } + + c.matchers = make([]matcher, 0, len(r.matchers)) + for _, m := range r.matchers { + c.matchers = append(c.matchers, m) + } + + return c +} + +func copyRouteRegexp(r *routeRegexp) *routeRegexp { + c := *r + return &c +} + +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. +func (r *Router) Match(req *http.Request, match *RouteMatch) bool { + for _, route := range r.routes { + if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } + return true + } + } + + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } + + return false + } + + // Closest match for a router (includes sub-routers) + if r.NotFoundHandler != nil { + match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound + return true + } + + match.MatchErr = ErrNotFound + return false +} + +// ServeHTTP dispatches the handler registered in the matched route. +// +// When there is a match, the route variables can be retrieved calling +// mux.Vars(request). +func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } + } + var match RouteMatch + var handler http.Handler + if r.Match(req, &match) { + handler = match.Handler + req = setVars(req, match.Vars) + req = setCurrentRoute(req, match.Route) + } + + if handler == nil && match.MatchErr == ErrMethodMismatch { + handler = methodNotAllowedHandler() + } + + if handler == nil { + handler = http.NotFoundHandler() + } + + handler.ServeHTTP(w, req) +} + +// Get returns a route registered with the given name. +func (r *Router) Get(name string) *Route { + return r.namedRoutes[name] +} + +// GetRoute returns a route registered with the given name. This method +// was renamed to Get() and remains here for backwards compatibility. +func (r *Router) GetRoute(name string) *Route { + return r.namedRoutes[name] +} + +// StrictSlash defines the trailing slash behavior for new routes. The initial +// value is false. +// +// When true, if the route path is "/path/", accessing "/path" will perform a redirect +// to the former and vice versa. In other words, your application will always +// see the path as specified in the route. +// +// When false, if the route path is "/path", accessing "/path/" will not match +// this route and vice versa. +// +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// +// Special case: when a route sets a path prefix using the PathPrefix() method, +// strict slash is ignored for that route because the redirect behavior can't +// be determined from a prefix alone. However, any subrouters created from that +// route inherit the original StrictSlash setting. +func (r *Router) StrictSlash(value bool) *Router { + r.strictSlash = value + return r +} + +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r +} + +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r +} + +// ---------------------------------------------------------------------------- +// Route factories +// ---------------------------------------------------------------------------- + +// NewRoute registers an empty route. +func (r *Router) NewRoute() *Route { + // initialize a route with a copy of the parent router's configuration + route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.routes = append(r.routes, route) + return route +} + +// Name registers a new route with a name. +// See Route.Name(). +func (r *Router) Name(name string) *Route { + return r.NewRoute().Name(name) +} + +// Handle registers a new route with a matcher for the URL path. +// See Route.Path() and Route.Handler(). +func (r *Router) Handle(path string, handler http.Handler) *Route { + return r.NewRoute().Path(path).Handler(handler) +} + +// HandleFunc registers a new route with a matcher for the URL path. +// See Route.Path() and Route.HandlerFunc(). +func (r *Router) HandleFunc(path string, f func(http.ResponseWriter, + *http.Request)) *Route { + return r.NewRoute().Path(path).HandlerFunc(f) +} + +// Headers registers a new route with a matcher for request header values. +// See Route.Headers(). +func (r *Router) Headers(pairs ...string) *Route { + return r.NewRoute().Headers(pairs...) +} + +// Host registers a new route with a matcher for the URL host. +// See Route.Host(). +func (r *Router) Host(tpl string) *Route { + return r.NewRoute().Host(tpl) +} + +// MatcherFunc registers a new route with a custom matcher function. +// See Route.MatcherFunc(). +func (r *Router) MatcherFunc(f MatcherFunc) *Route { + return r.NewRoute().MatcherFunc(f) +} + +// Methods registers a new route with a matcher for HTTP methods. +// See Route.Methods(). +func (r *Router) Methods(methods ...string) *Route { + return r.NewRoute().Methods(methods...) +} + +// Path registers a new route with a matcher for the URL path. +// See Route.Path(). +func (r *Router) Path(tpl string) *Route { + return r.NewRoute().Path(tpl) +} + +// PathPrefix registers a new route with a matcher for the URL path prefix. +// See Route.PathPrefix(). +func (r *Router) PathPrefix(tpl string) *Route { + return r.NewRoute().PathPrefix(tpl) +} + +// Queries registers a new route with a matcher for URL query values. +// See Route.Queries(). +func (r *Router) Queries(pairs ...string) *Route { + return r.NewRoute().Queries(pairs...) +} + +// Schemes registers a new route with a matcher for URL schemes. +// See Route.Schemes(). +func (r *Router) Schemes(schemes ...string) *Route { + return r.NewRoute().Schemes(schemes...) +} + +// BuildVarsFunc registers a new route with a custom function for modifying +// route variables before building a URL. +func (r *Router) BuildVarsFunc(f BuildVarsFunc) *Route { + return r.NewRoute().BuildVarsFunc(f) +} + +// Walk walks the router and all its sub-routers, calling walkFn for each route +// in the tree. The routes are walked in the order they were added. Sub-routers +// are explored depth-first. +func (r *Router) Walk(walkFn WalkFunc) error { + return r.walk(walkFn, []*Route{}) +} + +// SkipRouter is used as a return value from WalkFuncs to indicate that the +// router that walk is about to descend down to should be skipped. +var SkipRouter = errors.New("skip this router") + +// WalkFunc is the type of the function called for each route visited by Walk. +// At every invocation, it is given the current route, and the current router, +// and a list of ancestor routes that lead to the current route. +type WalkFunc func(route *Route, router *Router, ancestors []*Route) error + +func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { + for _, t := range r.routes { + err := walkFn(t, r, ancestors) + if err == SkipRouter { + continue + } + if err != nil { + return err + } + for _, sr := range t.matchers { + if h, ok := sr.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + if h, ok := t.handler.(*Router); ok { + ancestors = append(ancestors, t) + err := h.walk(walkFn, ancestors) + if err != nil { + return err + } + ancestors = ancestors[:len(ancestors)-1] + } + } + return nil +} + +// ---------------------------------------------------------------------------- +// Context +// ---------------------------------------------------------------------------- + +// RouteMatch stores information about a matched route. +type RouteMatch struct { + Route *Route + Handler http.Handler + Vars map[string]string + + // MatchErr is set to appropriate matching error + // It is set to ErrMethodMismatch if there is a mismatch in + // the request method and route method + MatchErr error +} + +type contextKey int + +const ( + varsKey contextKey = iota + routeKey +) + +// Vars returns the route variables for the current request, if any. +func Vars(r *http.Request) map[string]string { + if rv := contextGet(r, varsKey); rv != nil { + return rv.(map[string]string) + } + return nil +} + +// CurrentRoute returns the matched route for the current request, if any. +// This only works when called inside the handler of the matched route +// because the matched route is stored in the request context which is cleared +// after the handler returns, unless the KeepContext option is set on the +// Router. +func CurrentRoute(r *http.Request) *Route { + if rv := contextGet(r, routeKey); rv != nil { + return rv.(*Route) + } + return nil +} + +func setVars(r *http.Request, val interface{}) *http.Request { + return contextSet(r, varsKey, val) +} + +func setCurrentRoute(r *http.Request, val interface{}) *http.Request { + return contextSet(r, routeKey, val) +} + +// ---------------------------------------------------------------------------- +// Helpers +// ---------------------------------------------------------------------------- + +// cleanPath returns the canonical path for p, eliminating . and .. elements. +// Borrowed from the net/http package. +func cleanPath(p string) string { + if p == "" { + return "/" + } + if p[0] != '/' { + p = "/" + p + } + np := path.Clean(p) + // path.Clean removes trailing slash except for root; + // put the trailing slash back if necessary. + if p[len(p)-1] == '/' && np != "/" { + np += "/" + } + + return np +} + +// uniqueVars returns an error if two slices contain duplicated strings. +func uniqueVars(s1, s2 []string) error { + for _, v1 := range s1 { + for _, v2 := range s2 { + if v1 == v2 { + return fmt.Errorf("mux: duplicated route variable %q", v2) + } + } + } + return nil +} + +// checkPairs returns the count of strings passed in, and an error if +// the count is not an even number. +func checkPairs(pairs ...string) (int, error) { + length := len(pairs) + if length%2 != 0 { + return length, fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + } + return length, nil +} + +// mapFromPairsToString converts variadic string parameters to a +// string to string map. +func mapFromPairsToString(pairs ...string) (map[string]string, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]string, length/2) + for i := 0; i < length; i += 2 { + m[pairs[i]] = pairs[i+1] + } + return m, nil +} + +// mapFromPairsToRegex converts variadic string parameters to a +// string to regex map. +func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { + length, err := checkPairs(pairs...) + if err != nil { + return nil, err + } + m := make(map[string]*regexp.Regexp, length/2) + for i := 0; i < length; i += 2 { + regex, err := regexp.Compile(pairs[i+1]) + if err != nil { + return nil, err + } + m[pairs[i]] = regex + } + return m, nil +} + +// matchInArray returns true if the given string value is in the array. +func matchInArray(arr []string, value string) bool { + for _, v := range arr { + if v == value { + return true + } + } + return false +} + +// matchMapWithString returns true if the given key/value pairs exist in a given map. +func matchMapWithString(toCheck map[string]string, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != "" { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v == value { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// matchMapWithRegex returns true if the given key/value pairs exist in a given map compiled against +// the given regex +func matchMapWithRegex(toCheck map[string]*regexp.Regexp, toMatch map[string][]string, canonicalKey bool) bool { + for k, v := range toCheck { + // Check if key exists. + if canonicalKey { + k = http.CanonicalHeaderKey(k) + } + if values := toMatch[k]; values == nil { + return false + } else if v != nil { + // If value was defined as an empty string we only check that the + // key exists. Otherwise we also check for equality. + valueExists := false + for _, value := range values { + if v.MatchString(value) { + valueExists = true + break + } + } + if !valueExists { + return false + } + } + } + return true +} + +// methodNotAllowed replies to the request with an HTTP status code 405. +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) +} + +// methodNotAllowedHandler returns a simple request handler +// that replies to each request with a status code 405. +func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff --git a/vendor/github.com/gorilla/mux/regexp.go b/vendor/github.com/gorilla/mux/regexp.go new file mode 100644 index 0000000..ac1abcd --- /dev/null +++ b/vendor/github.com/gorilla/mux/regexp.go @@ -0,0 +1,345 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "bytes" + "fmt" + "net/http" + "net/url" + "regexp" + "strconv" + "strings" +) + +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = 0 + regexpTypeHost regexpType = 1 + regexpTypePrefix regexpType = 2 + regexpTypeQuery regexpType = 3 +) + +// newRouteRegexp parses a route template and returns a routeRegexp, +// used to match a host, a path or a query string. +// +// It will extract named variables, assemble a regexp to be matched, create +// a "reverse" template to build URLs and compile regexps to validate variable +// values used in URL building. +// +// Previously we accepted only Python-like identifiers for variable +// names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that +// name and pattern can't be empty, and names can't contain a colon. +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { + // Check if it is well-formed. + idxs, errBraces := braceIndices(tpl) + if errBraces != nil { + return nil, errBraces + } + // Backup the original. + template := tpl + // Now let's parse it. + defaultPattern := "[^/]+" + if typ == regexpTypeQuery { + defaultPattern = ".*" + } else if typ == regexpTypeHost { + defaultPattern = "[^.]+" + } + // Only match strict slash if not matching + if typ != regexpTypePath { + options.strictSlash = false + } + // Set a flag for strictSlash. + endSlash := false + if options.strictSlash && strings.HasSuffix(tpl, "/") { + tpl = tpl[:len(tpl)-1] + endSlash = true + } + varsN := make([]string, len(idxs)/2) + varsR := make([]*regexp.Regexp, len(idxs)/2) + pattern := bytes.NewBufferString("") + pattern.WriteByte('^') + reverse := bytes.NewBufferString("") + var end int + var err error + for i := 0; i < len(idxs); i += 2 { + // Set all values we are interested in. + raw := tpl[end:idxs[i]] + end = idxs[i+1] + parts := strings.SplitN(tpl[idxs[i]+1:end-1], ":", 2) + name := parts[0] + patt := defaultPattern + if len(parts) == 2 { + patt = parts[1] + } + // Name or pattern can't be empty. + if name == "" || patt == "" { + return nil, fmt.Errorf("mux: missing name or pattern in %q", + tpl[idxs[i]:end]) + } + // Build the regexp pattern. + fmt.Fprintf(pattern, "%s(?P<%s>%s)", regexp.QuoteMeta(raw), varGroupName(i/2), patt) + + // Build the reverse template. + fmt.Fprintf(reverse, "%s%%s", raw) + + // Append variable name and compiled pattern. + varsN[i/2] = name + varsR[i/2], err = regexp.Compile(fmt.Sprintf("^%s$", patt)) + if err != nil { + return nil, err + } + } + // Add the remaining. + raw := tpl[end:] + pattern.WriteString(regexp.QuoteMeta(raw)) + if options.strictSlash { + pattern.WriteString("[/]?") + } + if typ == regexpTypeQuery { + // Add the default pattern if the query value is empty + if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { + pattern.WriteString(defaultPattern) + } + } + if typ != regexpTypePrefix { + pattern.WriteByte('$') + } + + var wildcardHostPort bool + if typ == regexpTypeHost { + if !strings.Contains(pattern.String(), ":") { + wildcardHostPort = true + } + } + reverse.WriteString(raw) + if endSlash { + reverse.WriteByte('/') + } + // Compile full regexp. + reg, errCompile := regexp.Compile(pattern.String()) + if errCompile != nil { + return nil, errCompile + } + + // Check for capturing groups which used to work in older versions + if reg.NumSubexp() != len(idxs)/2 { + panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") + } + + // Done! + return &routeRegexp{ + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + wildcardHostPort: wildcardHostPort, + }, nil +} + +// routeRegexp stores a regexp to match a host or path and information to +// collect and validate route variables. +type routeRegexp struct { + // The unmodified template. + template string + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions + // Expanded regexp. + regexp *regexp.Regexp + // Reverse template. + reverse string + // Variable names. + varsN []string + // Variable regexps (validators). + varsR []*regexp.Regexp + // Wildcard host-port (no strict port match in hostname) + wildcardHostPort bool +} + +// Match matches the regexp against the URL host or path. +func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { + if r.regexpType == regexpTypeHost { + host := getHost(req) + if r.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + return r.regexp.MatchString(host) + } else { + if r.regexpType == regexpTypeQuery { + return r.matchQueryString(req) + } + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) + } +} + +// url builds a URL part using the given values. +func (r *routeRegexp) url(values map[string]string) (string, error) { + urlValues := make([]interface{}, len(r.varsN)) + for k, v := range r.varsN { + value, ok := values[v] + if !ok { + return "", fmt.Errorf("mux: missing route variable %q", v) + } + if r.regexpType == regexpTypeQuery { + value = url.QueryEscape(value) + } + urlValues[k] = value + } + rv := fmt.Sprintf(r.reverse, urlValues...) + if !r.regexp.MatchString(rv) { + // The URL is checked against the full regexp, instead of checking + // individual variables. This is faster but to provide a good error + // message, we check individual regexps if the URL doesn't match. + for k, v := range r.varsN { + if !r.varsR[k].MatchString(values[v]) { + return "", fmt.Errorf( + "mux: variable %q doesn't match, expected %q", values[v], + r.varsR[k].String()) + } + } + } + return rv, nil +} + +// getURLQuery returns a single query parameter from a request URL. +// For a URL with foo=bar&baz=ding, we return only the relevant key +// value pair for the routeRegexp. +func (r *routeRegexp) getURLQuery(req *http.Request) string { + if r.regexpType != regexpTypeQuery { + return "" + } + templateKey := strings.SplitN(r.template, "=", 2)[0] + for key, vals := range req.URL.Query() { + if key == templateKey && len(vals) > 0 { + return key + "=" + vals[0] + } + } + return "" +} + +func (r *routeRegexp) matchQueryString(req *http.Request) bool { + return r.regexp.MatchString(r.getURLQuery(req)) +} + +// braceIndices returns the first level curly brace indices from a string. +// It returns an error in case of unbalanced braces. +func braceIndices(s string) ([]int, error) { + var level, idx int + var idxs []int + for i := 0; i < len(s); i++ { + switch s[i] { + case '{': + if level++; level == 1 { + idx = i + } + case '}': + if level--; level == 0 { + idxs = append(idxs, idx, i+1) + } else if level < 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + } + } + if level != 0 { + return nil, fmt.Errorf("mux: unbalanced braces in %q", s) + } + return idxs, nil +} + +// varGroupName builds a capturing group name for the indexed variable. +func varGroupName(idx int) string { + return "v" + strconv.Itoa(idx) +} + +// ---------------------------------------------------------------------------- +// routeRegexpGroup +// ---------------------------------------------------------------------------- + +// routeRegexpGroup groups the route matchers that carry variables. +type routeRegexpGroup struct { + host *routeRegexp + path *routeRegexp + queries []*routeRegexp +} + +// setMatch extracts the variables from the URL once a route matches. +func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { + // Store host variables. + if v.host != nil { + host := getHost(req) + matches := v.host.regexp.FindStringSubmatchIndex(host) + if len(matches) > 0 { + extractVars(host, matches, v.host.varsN, m.Vars) + } + } + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Store path variables. + if v.path != nil { + matches := v.path.regexp.FindStringSubmatchIndex(path) + if len(matches) > 0 { + extractVars(path, matches, v.path.varsN, m.Vars) + // Check if we should redirect. + if v.path.options.strictSlash { + p1 := strings.HasSuffix(path, "/") + p2 := strings.HasSuffix(v.path.template, "/") + if p1 != p2 { + u, _ := url.Parse(req.URL.String()) + if p1 { + u.Path = u.Path[:len(u.Path)-1] + } else { + u.Path += "/" + } + m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently) + } + } + } + } + // Store query string variables. + for _, q := range v.queries { + queryURL := q.getURLQuery(req) + matches := q.regexp.FindStringSubmatchIndex(queryURL) + if len(matches) > 0 { + extractVars(queryURL, matches, q.varsN, m.Vars) + } + } +} + +// getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. +func getHost(r *http.Request) string { + if r.URL.IsAbs() { + return r.URL.Host + } + return r.Host +} + +func extractVars(input string, matches []int, names []string, output map[string]string) { + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] + } +} diff --git a/vendor/github.com/gorilla/mux/route.go b/vendor/github.com/gorilla/mux/route.go new file mode 100644 index 0000000..8479c68 --- /dev/null +++ b/vendor/github.com/gorilla/mux/route.go @@ -0,0 +1,710 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "regexp" + "strings" +) + +// Route stores information to match a request and build URLs. +type Route struct { + // Request handler for the route. + handler http.Handler + // If true, this route never matches: it is only used to build URLs. + buildOnly bool + // The name used to build URLs. + name string + // Error resulted from building a route. + err error + + // "global" reference to all named routes + namedRoutes map[string]*Route + + // config possibly passed in from `Router` + routeConf +} + +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. +func (r *Route) SkipClean() bool { + return r.skipClean +} + +// Match matches the route against the request. +func (r *Route) Match(req *http.Request, match *RouteMatch) bool { + if r.buildOnly || r.err != nil { + return false + } + + var matchErr error + + // Match everything. + for _, m := range r.matchers { + if matched := m.Match(req, match); !matched { + if _, ok := m.(methodMatcher); ok { + matchErr = ErrMethodMismatch + continue + } + + // Ignore ErrNotFound errors. These errors arise from match call + // to Subrouters. + // + // This prevents subsequent matching subrouters from failing to + // run middleware. If not ignored, the middleware would see a + // non-nil MatchErr and be skipped, even when there was a + // matching route. + if match.MatchErr == ErrNotFound { + match.MatchErr = nil + } + + matchErr = nil + return false + } + } + + if matchErr != nil { + match.MatchErr = matchErr + return false + } + + if match.MatchErr == ErrMethodMismatch { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler + } + + // Yay, we have a match. Let's collect some info about it. + if match.Route == nil { + match.Route = r + } + if match.Handler == nil { + match.Handler = r.handler + } + if match.Vars == nil { + match.Vars = make(map[string]string) + } + + // Set variables. + r.regexp.setMatch(req, match, r) + return true +} + +// ---------------------------------------------------------------------------- +// Route attributes +// ---------------------------------------------------------------------------- + +// GetError returns an error resulted from building the route, if any. +func (r *Route) GetError() error { + return r.err +} + +// BuildOnly sets the route to never match: it is only used to build URLs. +func (r *Route) BuildOnly() *Route { + r.buildOnly = true + return r +} + +// Handler -------------------------------------------------------------------- + +// Handler sets a handler for the route. +func (r *Route) Handler(handler http.Handler) *Route { + if r.err == nil { + r.handler = handler + } + return r +} + +// HandlerFunc sets a handler function for the route. +func (r *Route) HandlerFunc(f func(http.ResponseWriter, *http.Request)) *Route { + return r.Handler(http.HandlerFunc(f)) +} + +// GetHandler returns the handler for the route, if any. +func (r *Route) GetHandler() http.Handler { + return r.handler +} + +// Name ----------------------------------------------------------------------- + +// Name sets the name for the route, used to build URLs. +// It is an error to call Name more than once on a route. +func (r *Route) Name(name string) *Route { + if r.name != "" { + r.err = fmt.Errorf("mux: route already has name %q, can't set %q", + r.name, name) + } + if r.err == nil { + r.name = name + r.namedRoutes[name] = r + } + return r +} + +// GetName returns the name for the route, if any. +func (r *Route) GetName() string { + return r.name +} + +// ---------------------------------------------------------------------------- +// Matchers +// ---------------------------------------------------------------------------- + +// matcher types try to match a request. +type matcher interface { + Match(*http.Request, *RouteMatch) bool +} + +// addMatcher adds a matcher to the route. +func (r *Route) addMatcher(m matcher) *Route { + if r.err == nil { + r.matchers = append(r.matchers, m) + } + return r +} + +// addRegexpMatcher adds a host or path matcher and builder to a route. +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { + if r.err != nil { + return r.err + } + if typ == regexpTypePath || typ == regexpTypePrefix { + if len(tpl) > 0 && tpl[0] != '/' { + return fmt.Errorf("mux: path must start with a slash, got %q", tpl) + } + if r.regexp.path != nil { + tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl + } + } + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) + if err != nil { + return err + } + for _, q := range r.regexp.queries { + if err = uniqueVars(rr.varsN, q.varsN); err != nil { + return err + } + } + if typ == regexpTypeHost { + if r.regexp.path != nil { + if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { + return err + } + } + r.regexp.host = rr + } else { + if r.regexp.host != nil { + if err = uniqueVars(rr.varsN, r.regexp.host.varsN); err != nil { + return err + } + } + if typ == regexpTypeQuery { + r.regexp.queries = append(r.regexp.queries, rr) + } else { + r.regexp.path = rr + } + } + r.addMatcher(rr) + return nil +} + +// Headers -------------------------------------------------------------------- + +// headerMatcher matches the request against header values. +type headerMatcher map[string]string + +func (m headerMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithString(m, r.Header, true) +} + +// Headers adds a matcher for request header values. +// It accepts a sequence of key/value pairs to be matched. For example: +// +// r := mux.NewRouter() +// r.Headers("Content-Type", "application/json", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both request header values match. +// If the value is an empty string, it will match any value if the key is set. +func (r *Route) Headers(pairs ...string) *Route { + if r.err == nil { + var headers map[string]string + headers, r.err = mapFromPairsToString(pairs...) + return r.addMatcher(headerMatcher(headers)) + } + return r +} + +// headerRegexMatcher matches the request against the route given a regex for the header +type headerRegexMatcher map[string]*regexp.Regexp + +func (m headerRegexMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchMapWithRegex(m, r.Header, true) +} + +// HeadersRegexp accepts a sequence of key/value pairs, where the value has regex +// support. For example: +// +// r := mux.NewRouter() +// r.HeadersRegexp("Content-Type", "application/(text|json)", +// "X-Requested-With", "XMLHttpRequest") +// +// The above route will only match if both the request header matches both regular expressions. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. +func (r *Route) HeadersRegexp(pairs ...string) *Route { + if r.err == nil { + var headers map[string]*regexp.Regexp + headers, r.err = mapFromPairsToRegex(pairs...) + return r.addMatcher(headerRegexMatcher(headers)) + } + return r +} + +// Host ----------------------------------------------------------------------- + +// Host adds a matcher for the URL host. +// It accepts a template with zero or more URL variables enclosed by {}. +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next dot. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Host("www.example.com") +// r.Host("{subdomain}.domain.com") +// r.Host("{subdomain:[a-z]+}.domain.com") +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Host(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) + return r +} + +// MatcherFunc ---------------------------------------------------------------- + +// MatcherFunc is the function signature used by custom matchers. +type MatcherFunc func(*http.Request, *RouteMatch) bool + +// Match returns the match for a given request. +func (m MatcherFunc) Match(r *http.Request, match *RouteMatch) bool { + return m(r, match) +} + +// MatcherFunc adds a custom function to be used as request matcher. +func (r *Route) MatcherFunc(f MatcherFunc) *Route { + return r.addMatcher(f) +} + +// Methods -------------------------------------------------------------------- + +// methodMatcher matches the request against HTTP methods. +type methodMatcher []string + +func (m methodMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.Method) +} + +// Methods adds a matcher for HTTP methods. +// It accepts a sequence of one or more methods to be matched, e.g.: +// "GET", "POST", "PUT". +func (r *Route) Methods(methods ...string) *Route { + for k, v := range methods { + methods[k] = strings.ToUpper(v) + } + return r.addMatcher(methodMatcher(methods)) +} + +// Path ----------------------------------------------------------------------- + +// Path adds a matcher for the URL path. +// It accepts a template with zero or more URL variables enclosed by {}. The +// template must start with a "/". +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +// +// For example: +// +// r := mux.NewRouter() +// r.Path("/products/").Handler(ProductsHandler) +// r.Path("/products/{key}").Handler(ProductsHandler) +// r.Path("/articles/{category}/{id:[0-9]+}"). +// Handler(ArticleHandler) +// +// Variable names must be unique in a given route. They can be retrieved +// calling mux.Vars(request). +func (r *Route) Path(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePath) + return r +} + +// PathPrefix ----------------------------------------------------------------- + +// PathPrefix adds a matcher for the URL path prefix. This matches if the given +// template is a prefix of the full URL path. See Route.Path() for details on +// the tpl argument. +// +// Note that it does not treat slashes specially ("/foobar/" will be matched by +// the prefix "/foo") so you may want to use a trailing slash here. +// +// Also note that the setting of Router.StrictSlash() has no effect on routes +// with a PathPrefix matcher. +func (r *Route) PathPrefix(tpl string) *Route { + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) + return r +} + +// Query ---------------------------------------------------------------------- + +// Queries adds a matcher for URL query values. +// It accepts a sequence of key/value pairs. Values may define variables. +// For example: +// +// r := mux.NewRouter() +// r.Queries("foo", "bar", "id", "{id:[0-9]+}") +// +// The above route will only match if the URL contains the defined queries +// values, e.g.: ?foo=bar&id=42. +// +// If the value is an empty string, it will match any value if the key is set. +// +// Variables can define an optional regexp pattern to be matched: +// +// - {name} matches anything until the next slash. +// +// - {name:pattern} matches the given regexp pattern. +func (r *Route) Queries(pairs ...string) *Route { + length := len(pairs) + if length%2 != 0 { + r.err = fmt.Errorf( + "mux: number of parameters must be multiple of 2, got %v", pairs) + return nil + } + for i := 0; i < length; i += 2 { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { + return r + } + } + + return r +} + +// Schemes -------------------------------------------------------------------- + +// schemeMatcher matches the request against URL schemes. +type schemeMatcher []string + +func (m schemeMatcher) Match(r *http.Request, match *RouteMatch) bool { + return matchInArray(m, r.URL.Scheme) +} + +// Schemes adds a matcher for URL schemes. +// It accepts a sequence of schemes to be matched, e.g.: "http", "https". +func (r *Route) Schemes(schemes ...string) *Route { + for k, v := range schemes { + schemes[k] = strings.ToLower(v) + } + if len(schemes) > 0 { + r.buildScheme = schemes[0] + } + return r.addMatcher(schemeMatcher(schemes)) +} + +// BuildVarsFunc -------------------------------------------------------------- + +// BuildVarsFunc is the function signature used by custom build variable +// functions (which can modify route variables before a route's URL is built). +type BuildVarsFunc func(map[string]string) map[string]string + +// BuildVarsFunc adds a custom function to be used to modify build variables +// before a route's URL is built. +func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { + if r.buildVarsFunc != nil { + // compose the old and new functions + old := r.buildVarsFunc + r.buildVarsFunc = func(m map[string]string) map[string]string { + return f(old(m)) + } + } else { + r.buildVarsFunc = f + } + return r +} + +// Subrouter ------------------------------------------------------------------ + +// Subrouter creates a subrouter for the route. +// +// It will test the inner routes only if the parent route matched. For example: +// +// r := mux.NewRouter() +// s := r.Host("www.example.com").Subrouter() +// s.HandleFunc("/products/", ProductsHandler) +// s.HandleFunc("/products/{key}", ProductHandler) +// s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +// +// Here, the routes registered in the subrouter won't be tested if the host +// doesn't match. +func (r *Route) Subrouter() *Router { + // initialize a subrouter with a copy of the parent route's configuration + router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} + r.addMatcher(router) + return router +} + +// ---------------------------------------------------------------------------- +// URL building +// ---------------------------------------------------------------------------- + +// URL builds a URL for the route. +// +// It accepts a sequence of key/value pairs for the route variables. For +// example, given this route: +// +// r := mux.NewRouter() +// r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// ...a URL for it can be built using: +// +// url, err := r.Get("article").URL("category", "technology", "id", "42") +// +// ...which will return an url.URL with the following path: +// +// "/articles/technology/42" +// +// This also works for host variables: +// +// r := mux.NewRouter() +// r.Host("{subdomain}.domain.com"). +// HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler). +// Name("article") +// +// // url.String() will be "http://news.domain.com/articles/technology/42" +// url, err := r.Get("article").URL("subdomain", "news", +// "category", "technology", +// "id", "42") +// +// All variables defined in the route are required, and their values must +// conform to the corresponding patterns. +func (r *Route) URL(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + var scheme, host, path string + queries := make([]string, 0, len(r.regexp.queries)) + if r.regexp.host != nil { + if host, err = r.regexp.host.url(values); err != nil { + return nil, err + } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } + } + if r.regexp.path != nil { + if path, err = r.regexp.path.url(values); err != nil { + return nil, err + } + } + for _, q := range r.regexp.queries { + var query string + if query, err = q.url(values); err != nil { + return nil, err + } + queries = append(queries, query) + } + return &url.URL{ + Scheme: scheme, + Host: host, + Path: path, + RawQuery: strings.Join(queries, "&"), + }, nil +} + +// URLHost builds the host part of the URL for a route. See Route.URL(). +// +// The route must have a host defined. +func (r *Route) URLHost(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.host == nil { + return nil, errors.New("mux: route doesn't have a host") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + host, err := r.regexp.host.url(values) + if err != nil { + return nil, err + } + u := &url.URL{ + Scheme: "http", + Host: host, + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil +} + +// URLPath builds the path part of the URL for a route. See Route.URL(). +// +// The route must have a path defined. +func (r *Route) URLPath(pairs ...string) (*url.URL, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.path == nil { + return nil, errors.New("mux: route doesn't have a path") + } + values, err := r.prepareVars(pairs...) + if err != nil { + return nil, err + } + path, err := r.regexp.path.url(values) + if err != nil { + return nil, err + } + return &url.URL{ + Path: path, + }, nil +} + +// GetPathTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route doesn't have a path") + } + return r.regexp.path.template, nil +} + +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetQueriesRegexp returns the expanded regular expressions used to match the +// route queries. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not have queries. +func (r *Route) GetQueriesRegexp() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.regexp.String()) + } + return queries, nil +} + +// GetQueriesTemplates returns the templates used to build the +// query matching. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define queries. +func (r *Route) GetQueriesTemplates() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.template) + } + return queries, nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, errors.New("mux: route doesn't have methods") +} + +// GetHostTemplate returns the template used to build the +// route match. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a host. +func (r *Route) GetHostTemplate() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.host == nil { + return "", errors.New("mux: route doesn't have a host") + } + return r.regexp.host.template, nil +} + +// prepareVars converts the route variable pairs into a map. If the route has a +// BuildVarsFunc, it is invoked. +func (r *Route) prepareVars(pairs ...string) (map[string]string, error) { + m, err := mapFromPairsToString(pairs...) + if err != nil { + return nil, err + } + return r.buildVars(m), nil +} + +func (r *Route) buildVars(m map[string]string) map[string]string { + if r.buildVarsFunc != nil { + m = r.buildVarsFunc(m) + } + return m +} diff --git a/vendor/github.com/gorilla/mux/test_helpers.go b/vendor/github.com/gorilla/mux/test_helpers.go new file mode 100644 index 0000000..32ecffd --- /dev/null +++ b/vendor/github.com/gorilla/mux/test_helpers.go @@ -0,0 +1,19 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return setVars(r, val) +} diff --git a/videos/README.md b/videos/README.md new file mode 100644 index 0000000..4434d5f --- /dev/null +++ b/videos/README.md @@ -0,0 +1 @@ +Your videos go here. \ No newline at end of file