giffer

Create .gif images from sites like youtube.com
Log | Files | Refs | LICENSE

commit 83707e293f4d80ac600bde042c36b8009fa7bf9f
parent 12a96d1657062db54156c0647335b9c20b612c16
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Thu,  8 Nov 2018 14:41:48 +1300

[+] Add Gif handling endpoint.

Diffstat:
Acmd/desktop/giffer.go | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/desktop/http.go | 154+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/desktop/main.go | 306++-----------------------------------------------------------------------------
Acmd/desktop/server.go | 197+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcmd/desktop/ui/src/App.vue | 54+++++++++++++++++++++++++++++++++++++-----------------
Mgo.mod | 8++++----
Mgo.sum | 19++++++++++---------
7 files changed, 512 insertions(+), 331 deletions(-)

diff --git a/cmd/desktop/giffer.go b/cmd/desktop/giffer.go @@ -0,0 +1,105 @@ +package main + +import ( + "bytes" + "image" + "image/gif" + "log" + "path/filepath" + "strings" + "sync" + + "github.com/disintegration/imaging" + "github.com/jackmordaunt/giffer" + "github.com/pkg/errors" +) + +// Giffer wraps the giffer business logic. +type Giffer struct { + *giffer.Downloader + *giffer.FFMpeg +} + +// GififyURL downloads the video at url and creates a .gif based on the spcified +// parameters. +func (g Giffer) GififyURL( + url string, + start, end, fps float64, + width, height int, + q giffer.Quality, +) (*RenderedGif, error) { + videofile, err := g.Download(url, q) + if err != nil { + return nil, errors.Wrap(err, "downloading") + } + frames, err := g.Extract(videofile, start, end, fps) + if err != nil { + return nil, errors.Wrap(err, "extracting frames") + } + type processed struct { + Img *image.Paletted + Index int + } + images := make(chan processed) + wg := &sync.WaitGroup{} + wg.Add(len(frames)) + for ii, frame := range frames { + ii := ii + frame := frame + go func() { + defer wg.Done() + if width != 0 || height != 0 { + frame = imaging.Resize(frame, width, height, imaging.Box) + } + buf := bytes.Buffer{} + if err := gif.Encode(&buf, frame, nil); err != nil { + log.Printf("encoding gif: %v", err) + return + } + tmpimg, err := gif.Decode(&buf) + if err != nil { + log.Printf("decoding gif: %v", err) + return + } + images <- processed{ + Img: tmpimg.(*image.Paletted), + Index: ii, + } + }() + } + go func() { + wg.Wait() + close(images) + }() + paletted := make([]*image.Paletted, len(frames)) + for frame := range images { + paletted[frame.Index] = frame.Img + } + delays := make([]int, len(frames)) + delay := int(100 / fps) + for ii := range delays { + delays[ii] = delay + } + buf := bytes.NewBuffer(nil) + cfg := &gif.GIF{ + Image: paletted, + Delay: delays, + LoopCount: 0, + } + if err := gif.EncodeAll(buf, cfg); err != nil { + return nil, errors.Wrap(err, "encoding animated gif") + } + r := &RenderedGif{ + Buffer: buf, + // Keep the title but replace the .mp4 extension with .gif + FileName: sanitiseFilepath(strings.Split(filepath.Base(videofile), ".")[0] + ".gif"), + } + return r, nil +} + +// RenderedGif wraps the gif data with some metadata. +type RenderedGif struct { + *bytes.Buffer + // FileName is <title>.<ext> + FileName string +} diff --git a/cmd/desktop/http.go b/cmd/desktop/http.go @@ -0,0 +1,154 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/http/httputil" + "net/url" + "runtime/debug" + "strings" + + "github.com/pkg/errors" +) + +// Proxy incoming requests to target. +type Proxy struct { + Target *url.URL +} + +func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + proxy := httputil.NewSingleHostReverseProxy(p.Target) + r.URL.Host = p.Target.Host + r.URL.Scheme = p.Target.Scheme + r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) + r.Host = p.Target.Host + proxy.ServeHTTP(w, r) +} + +// Log creates middleware that logs requests. +// If out is nil it is assumed that no logs are desired. +type Log struct { + Logger *log.Logger + ShowBody bool +} + +// Middleware the handler with logging middleware. +func (l Log) Middleware(next http.Handler) http.Handler { + if l.Logger == nil { + return next + } + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var msg = &strings.Builder{} + fmt.Fprintf(msg, "URL: %s\n", r.URL) + fmt.Fprintf(msg, "Method: %s\n", r.Method) + fmt.Fprintf(msg, "Headers: http.Header{\n") + for h, v := range r.Header { + fmt.Fprintf(msg, "\t%q: {", h) + for ii := range v { + fmt.Fprintf(msg, "%s", v[ii]) + if ii != len(v)-1 { + fmt.Fprint(msg, ",") + } + } + fmt.Fprintf(msg, "},\n") + } + fmt.Fprintf(msg, "}\n") + if r.Response != nil { + fmt.Fprintf(msg, "Status Code: %d\n", r.Response.StatusCode) + } + if l.ShowBody { + by, _ := readUntil(r.Body, 1000*32) // 32KB + if len(by) > 0 { + fmt.Fprintf(msg, " Body: %s\n", string(by)) + } + r.Body = ProxyCloser{ + Reader: io.MultiReader(bytes.NewBuffer(by), r.Body), + Closer: r.Body, + } + } + fmt.Fprintf(msg, "\n") + l.Logger.Printf(msg.String()) + next.ServeHTTP(w, r) + }) +} + +func readUntil(r io.Reader, until int64) ([]byte, error) { + buf := bytes.NewBuffer(nil) + if _, err := io.CopyN(buf, r, until); err != nil && err != io.EOF { + return buf.Bytes(), err + } + return buf.Bytes(), nil +} + +func sanitiseFilepath(p string) string { + r := strings.NewReplacer( + "(", "", + ")", "", + "!", "", + "+", "", + "?", "", + "*", "", + "&", "", + "^", "", + "=", "", + " ", "", + ) + return r.Replace(p) +} + +// ProxyCloser decouples the reader from the closer. +// This enables the use of io.MultiReader while closing the underlying source. +type ProxyCloser struct { + io.Reader + io.Closer +} + +// LogWriteHeaderErrors helps when debugging http "multiple response.WriteHeader" +// calls. +type LogWriteHeaderErrors struct { + Out io.Writer +} + +func (d LogWriteHeaderErrors) Write(p []byte) (n int, err error) { + s := string(p) + if strings.Contains(s, "multiple response.WriteHeader") { + n, err = d.Out.Write(debug.Stack()) + if err != nil { + return n, err + } + } + return d.Out.Write(p) +} + +// FIXME(jfm): The error should be logged instead of returned to client, such +// that it makes it's way to the developers. +// You don't want clients knowing your failure modes, since this can lead +// to security exploits. +func httpError(w http.ResponseWriter, err error) { + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +func writeJSON(w http.ResponseWriter, v interface{}) { + type appErr struct { + Error error `json:"error,omitempty"` + } + // Wrap an error to ensure the returned object has an "error" property. + if err, ok := v.(error); ok { + v = appErr{ + Error: err, + } + } + by, err := json.Marshal(v) + if err != nil { + httpError(w, errors.Wrap(err, "creating json response")) + return + } + if _, err := w.Write(by); err != nil { + // log? panic? + panic(errors.Wrap(err, "atttempting to write to http.ResponseWriter")) + } +} diff --git a/cmd/desktop/main.go b/cmd/desktop/main.go @@ -1,28 +1,16 @@ package main import ( - "runtime/debug" - "bytes" - "encoding/json" "flag" "fmt" - "image" - "image/gif" - "io" "log" "net/http" - "net/http/httputil" "net/url" - "os" - "path/filepath" - "strings" - "sync" + "time" - "github.com/disintegration/imaging" "github.com/jackmordaunt/giffer" "github.com/GeertJohan/go.rice" - "github.com/pkg/errors" "github.com/gorilla/mux" @@ -65,12 +53,10 @@ func main() { Static: static, } svr := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: ui, - // Can't use these time-outs yet since the gifify route doesn't - // return immediately (downloads 400MB, etc...). - // WriteTimeout: 15 * time.Second, - // ReadTimeout: 15 * time.Second, + Addr: fmt.Sprintf(":%s", port), + Handler: ui, + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, } go func() { if err := svr.ListenAndServe(); err != nil { @@ -87,285 +73,3 @@ func main() { }) view.Run() } - -// UI serves the user interface over http. -type UI struct { - App *Giffer - Router *mux.Router - Static http.Handler - init sync.Once -} - -func (ui *UI) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ui.init.Do(func() { - ui.routes() - }) - ui.Router.ServeHTTP(w, r) -} - -func (ui *UI) routes() { - log := Log{ - Logger: log.New(LogWriteHeaderErrors{Out: os.Stdout}, "", 0), - ShowBody: true, - } - ui.Router.Handle("/gifify", Wrap(ui.gifify(), log)) - // The router typically treats "/" as a unique router We have to tell - // it otherwise in order to handle file paths correctly. - ui.Router.Handle("/{path:.*}", Wrap(ui.Static, log)) -} - -func (ui *UI) gifify() http.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - type request struct { - URL string `json:"url,omitempty"` - Start float64 `json:"start,omitempty"` - End float64 `json:"end,omitempty"` - FPS float64 `json:"fps,omitempty"` - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - Output string `json:"output,omitempty"` - Quality int `json:"quality,omitempty"` - } - var req request - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - ui.error(w, errors.Wrap(err, "decoding json request")) - return - } - img, err := ui.App.GififyURL( - req.URL, - req.Start, - req.End, - req.FPS, - req.Width, - req.Height, - giffer.Quality(req.Quality)) - if err != nil { - ui.error(w, err) - return - } - w.Header().Set("Content-Type", http.DetectContentType(img.Bytes())) - w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, img.FileName)) - if _, err := io.Copy(w, img); err != nil { - ui.error(w, errors.Wrap(err, "writing response")) - return - } - } -} - -func (ui *UI) error(w http.ResponseWriter, err error) { - http.Error(w, err.Error(), http.StatusInternalServerError) -} - -// Giffer wraps the giffer business logic. -type Giffer struct { - *giffer.Downloader - *giffer.FFMpeg -} - -// GififyURL downloads the video at url and creates a .gif based on the spcified -// parameters. -func (g Giffer) GififyURL( - url string, - start, end, fps float64, - width, height int, - q giffer.Quality, -) (*RenderedGif, error) { - videofile, err := g.Download(url, q) - if err != nil { - return nil, errors.Wrap(err, "downloading") - } - frames, err := g.Extract(videofile, start, end, fps) - if err != nil { - return nil, errors.Wrap(err, "extracting frames") - } - type processed struct { - Img *image.Paletted - Index int - } - images := make(chan processed) - wg := &sync.WaitGroup{} - wg.Add(len(frames)) - for ii, frame := range frames { - ii := ii - frame := frame - go func() { - defer wg.Done() - if width != 0 || height != 0 { - frame = imaging.Resize(frame, width, height, imaging.Box) - } - buf := bytes.Buffer{} - if err := gif.Encode(&buf, frame, nil); err != nil { - // errors.Wrap(err, "encoding gif") - return - } - tmpimg, err := gif.Decode(&buf) - if err != nil { - // errors.Wrap(err, "decoding gif") - return - } - images <- processed{ - Img: tmpimg.(*image.Paletted), - Index: ii, - } - }() - } - go func() { - wg.Wait() - close(images) - }() - paletted := make([]*image.Paletted, len(frames)) - for frame := range images { - paletted[frame.Index] = frame.Img - } - delays := make([]int, len(frames)) - delay := int(100 / fps) - for ii := range delays { - delays[ii] = delay - } - buf := bytes.NewBuffer(nil) - cfg := &gif.GIF{ - Image: paletted, - Delay: delays, - LoopCount: 0, - } - if err := gif.EncodeAll(buf, cfg); err != nil { - return nil, errors.Wrap(err, "encoding animated gif") - } - r := &RenderedGif{ - Buffer: buf, - // Keep the title but replace the .mp4 extension with .gif - FileName: sanitiseFilepath(strings.Split(filepath.Base(videofile), ".")[0] + ".gif"), - } - return r, nil -} - -// RenderedGif wraps the gif data with some metadata. -type RenderedGif struct { - *bytes.Buffer - // FileName is <title>.<ext> - FileName string -} - -// Proxy incoming requests to target. -type Proxy struct { - Target *url.URL -} - -func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - proxy := httputil.NewSingleHostReverseProxy(p.Target) - r.URL.Host = p.Target.Host - r.URL.Scheme = p.Target.Scheme - r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) - r.Host = p.Target.Host - proxy.ServeHTTP(w, r) -} - -// Log creates middleware that logs requests. -// If out is nil it is assumed that no logs are desired. -type Log struct { - Logger *log.Logger - ShowBody bool -} - -// Wrap the handler with logging middleware. -func (l Log) Wrap(next http.Handler) http.Handler { - if l.Logger == nil { - return next - } - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var msg = &strings.Builder{} - fmt.Fprintf(msg, "URL: %s\n", r.URL) - fmt.Fprintf(msg, "Method: %s\n", r.Method) - fmt.Fprintf(msg, "Headers: http.Header{\n") - for h, v := range r.Header { - fmt.Fprintf(msg, "\t%q: {", h) - for ii := range v { - fmt.Fprintf(msg, "%s", v[ii]) - if ii != len(v)-1 { - fmt.Fprint(msg, ",") - } - } - fmt.Fprintf(msg, "},\n") - } - fmt.Fprintf(msg, "}\n") - if r.Response != nil { - fmt.Fprintf(msg, "Status Code: %d\n", r.Response.StatusCode) - } - if l.ShowBody { - by, _ := readUntil(r.Body, 1000*32) // 32KB - if len(by) > 0 { - fmt.Fprintf(msg, " Body: %s\n", string(by)) - } - r.Body = ProxyCloser{ - Reader: io.MultiReader(bytes.NewBuffer(by), r.Body), - Closer: r.Body, - } - } - fmt.Fprintf(msg, "\n") - l.Logger.Printf(msg.String()) - next.ServeHTTP(w, r) - }) -} - -func readUntil(r io.Reader, until int64) ([]byte, error) { - buf := bytes.NewBuffer(nil) - if _, err := io.CopyN(buf, r, until); err != nil && err != io.EOF { - return buf.Bytes(), err - } - return buf.Bytes(), nil -} - -// Wrapper wraps an http.Handler to provide things like middleware. -type Wrapper interface { - Wrap(http.Handler) http.Handler -} - -// Wrap the handler with the provided wrappers. -func Wrap(h http.Handler, wrappers ...Wrapper) http.Handler { - for _, w := range wrappers { - h = w.Wrap(h) - } - return h -} - -func sanitiseFilepath(p string) string { - r := strings.NewReplacer( - "(", "", - ")", "", - "!", "", - "+", "", - "?", "", - "*", "", - "&", "", - "^", "", - "=", "", - " ", "", - ) - return r.Replace(p) -} - -// ProxyCloser decouples the reader from the closer. -// This enables the use of io.MultiReader while closing the underlying source. -type ProxyCloser struct { - io.Reader - io.Closer -} - - -// LogWriteHeaderErrors helps when debugging http "multiple response.WriteHeader" -// calls. -type LogWriteHeaderErrors struct { - Out io.Writer -} - -func (d LogWriteHeaderErrors) Write(p []byte) (n int, err error) { - s := string(p) - if strings.Contains(s, "multiple response.WriteHeader") { - n, err = d.Out.Write(debug.Stack()) - if err != nil { - return n, err - } - } - return d.Out.Write(p) -} diff --git a/cmd/desktop/server.go b/cmd/desktop/server.go @@ -0,0 +1,197 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/OneOfOne/xxhash" + "github.com/jackmordaunt/giffer" + + "github.com/gorilla/mux" + "github.com/gorilla/websocket" + + "github.com/pkg/errors" +) + +// UI serves the user interface over http. +type UI struct { + App *Giffer + Router *mux.Router + Static http.Handler + + gifmap map[string]http.Handler + init sync.Once +} + +func (ui *UI) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ui.init.Do(func() { + if ui.gifmap == nil { + ui.gifmap = make(map[string]http.Handler) + } + ui.routes() + }) + ui.Router.ServeHTTP(w, r) +} + +func (ui *UI) routes() { + log := Log{ + Logger: log.New(LogWriteHeaderErrors{Out: os.Stdout}, "", 0), + ShowBody: true, + } + ui.Router.Use(log.Middleware) + ui.Router.Handle("/gifify", ui.gifify()) + ui.Router.Handle("/gifs/{key}", ui.gifs()) + ui.Router.Handle("/gifs/{key}/info", ui.gifs()) + ui.Router.Handle("/{path:.*}", ui.Static) +} + +func (ui *UI) gifify() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + type request struct { + URL string `json:"url,omitempty"` + Start float64 `json:"start,omitempty"` + End float64 `json:"end,omitempty"` + FPS float64 `json:"fps,omitempty"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Output string `json:"output,omitempty"` + Quality int `json:"quality,omitempty"` + } + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + httpError(w, errors.Wrap(err, "decoding json request")) + return + } + by, err := json.Marshal(req) + if err != nil { + httpError(w, fmt.Errorf("marshalling json for hash")) + return + } + h := xxhash.New64() + if _, err := h.Write(by); err != nil { + httpError(w, fmt.Errorf("writing to hash object")) + return + } + g := &Gif{ + Upgrader: &websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + }, + } + go g.Process(func() (*RenderedGif, error) { + return ui.App.GififyURL( + req.URL, + req.Start, + req.End, + req.FPS, + req.Width, + req.Height, + giffer.Quality(req.Quality)) + }) + key := fmt.Sprintf("%d", h.Sum64()) + ui.gifmap[key] = g + type response struct { + File string `json:"file"` + Info string `json:"info"` + } + // FIXME(jfm): Should these endpoints be typed, instead of + // relying on assumptions about the routing? + writeJSON(w, response{ + File: fmt.Sprintf("/gifs/%s", key), + Info: fmt.Sprintf("/gifs/%s/info", key), + }) + } +} + +func (ui *UI) gifs() http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + key := mux.Vars(r)["key"] + h, ok := ui.gifmap[key] + if !ok { + writeJSON(w, fmt.Errorf("no gif exists for key %q", key)) + return + } + h.ServeHTTP(w, r) + } +} + +// Gif handles the serving of a gif file. +// There are two enpoints: +type Gif struct { + Upgrader *websocket.Upgrader + Tick time.Duration + + file *RenderedGif + subs map[*websocket.Conn]struct{} + subMutex sync.Mutex + err error + once sync.Once +} + +// Process runs the specified function and sends a websocket message when it +// completes. +func (g *Gif) Process(fn func() (*RenderedGif, error)) { + type done struct { + Err error `json:"error,omitempty"` + } + g.file, g.err = fn() + g.subMutex.Lock() + for s := range g.subs { + err := s.WriteJSON(done{ + Err: g.err, + }) + if err != nil { + s.Close() + delete(g.subs, s) + log.Printf("writing json to websocket: %v", err) + } + } + g.subMutex.Unlock() +} + +func (g *Gif) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "info") { + g.subscribe(w, r) + } else { + g.serveFile(w, r) + } +} + +func (g *Gif) subscribe(w http.ResponseWriter, r *http.Request) { + c, err := g.Upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("upgrading websocket: %v", err) + return + } + g.append(c) +} + +func (g *Gif) serveFile(w http.ResponseWriter, r *http.Request) { + if g.file == nil { + writeJSON(w, fmt.Errorf("gif not ready")) + return + } + w.Header().Set("Content-Type", "image/gif") + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, g.file.FileName)) + if _, err := io.Copy(w, g.file); err != nil { + httpError(w, errors.Wrap(err, "writing gif to response body")) + return + } +} + +func (g *Gif) append(c *websocket.Conn) { + g.once.Do(func() { + g.subs = make(map[*websocket.Conn]struct{}) + }) + g.subMutex.Lock() + g.subs[c] = struct{}{} + g.subMutex.Unlock() +} diff --git a/cmd/desktop/ui/src/App.vue b/cmd/desktop/ui/src/App.vue @@ -50,12 +50,12 @@ </span> </form> </div> - <div id="img" class="row"> - + <div class="row"> + <a v-if="link.length > 0" download="memer.gif" :href="link">Download!</a> </div> <div class="row"> - <pre v-if="errors.length > 0" class="errors"> - {{errors}} + <pre v-for="(err, ii) in errors" :key="ii" class="errors"> + {{err}} </pre> </div> <div class="row"> @@ -84,28 +84,48 @@ export default { }, loading: false, errors: [], + link: "", } }, methods: { submit() { - console.log("submit") this.loading = true - this.form.quality = Number(this.form.quality) + try { + this.form.start = Number(this.form.start) + this.form.end = Number(this.form.end) + this.form.width = Number(this.form.width) + this.form.height = Number(this.form.height) + this.form.fps = Number(this.form.fps) + this.form.quality = Number(this.form.quality) + } catch(e) { + this.errors.push("form values are not valid numbers") + return + } axios.post("gifify", this.form) .then(resp => { - let mime = resp.headers["content-type"] - console.log(mime) - let decoded = btoa(resp.data) - let img = new ImageData() - img.src = `data:${mime};base64,${decoded}` - document.getElementById("#img").appendChild(img) + console.log(resp) + if (resp.data.error) { + this.errors.push(resp.data.error) + return + } + console.log("waiting for download to be ready...") + let { file, info } = resp.data + let updates = new WebSocket(`ws://localhost:8081${info}`) + updates.onmessage = (msg) => { + if (msg.error !== undefined) { + this.errors.push(msg.error) + return + } + console.log("ready to download!") + this.link = `http://localhost:8081${file}` + updates.close() + this.loading = false + } + }) .catch(err => { - console.log(err) - this.errors.push(err) - }) - .finally(() => { - this.loading = false + console.log("catching error") + this.errors.push(err.response.data) }) } }, diff --git a/go.mod b/go.mod @@ -1,16 +1,16 @@ module github.com/jackmordaunt/giffer require ( - github.com/GeertJohan/go.incremental v0.0.0-20161212213043-1172aab96510 // indirect github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da - github.com/akavel/rsrc v0.0.0-20170831122431-f6a15ece2cfd // indirect + github.com/OneOfOne/xxhash v1.2.2 + github.com/cespare/xxhash v1.1.0 // indirect github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb // indirect github.com/disintegration/imaging v1.5.0 + github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.2 + github.com/gorilla/websocket v1.4.0 github.com/jackmordaunt/video-downloader v0.0.0-20181105210957-4fcc0a0db5fa - github.com/jessevdk/go-flags v1.4.0 // indirect github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 // indirect - github.com/kr/pty v1.1.3 // indirect github.com/pkg/errors v0.8.0 github.com/zserge/webview v0.0.0-20181018084947-f390a2df9ec5 golang.org/x/image v0.0.0-20181102021609-63626fb251ce // indirect diff --git a/go.sum b/go.sum @@ -1,35 +1,34 @@ -github.com/GeertJohan/go.incremental v0.0.0-20161212213043-1172aab96510 h1:XKmpFaGpsBo5B7NC6RxawBYk6BFi0a6fw03J5PYW/9g= -github.com/GeertJohan/go.incremental v0.0.0-20161212213043-1172aab96510/go.mod h1:0K8QLSiwClOppBKLLSRX1sFvYdX5/fWqAZkjboOEzak= github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da h1:UVU3a9pRUyLdnBtn60WjRl0s4SEyJc2ChCY56OAR6wI= github.com/GeertJohan/go.rice v0.0.0-20170420135705-c02ca9a983da/go.mod h1:DgrzXonpdQbfN3uYaGz1EG4Sbhyum/MMIn6Cphlh2bw= github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403 h1:EtZwYyLbkEcIt+B//6sujwRCnHuTEK3qiSypAX5aJeM= github.com/MercuryEngineering/CookieMonster v0.0.0-20180304172713-1584578b3403/go.mod h1:mM6WvakkX2m+NgMiPCfFFjwfH4KzENC07zeGEqq9U7s= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.4.1 h1:smcIRGdYm/w7JSbcdeLHEMzxmsBQvl8lhf0dSw2nzMI= github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA= -github.com/akavel/rsrc v0.0.0-20170831122431-f6a15ece2cfd h1:yumR8733CaQ3P76MFbIbBKdrJmy4EqnQ5DIhqq8gq2Q= -github.com/akavel/rsrc v0.0.0-20170831122431-f6a15ece2cfd/go.mod h1:2+aQMrY0hBFBaIr2xxnZ/ctfwnYmMRMbTczYLAC34v4= github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb h1:tUf55Po0vzOendQ7NWytcdK0VuzQmfAgvGBUOQvN0WA= github.com/daaku/go.zipexe v0.0.0-20150329023125-a5fe2436ffcb/go.mod h1:U0vRfAucUOohvdCxt5MWLF+TePIL0xbCkbKIiV8TQCE= github.com/disintegration/imaging v1.5.0 h1:uYqUhwNmLU4K1FN44vhqS4TZJRAA4RhBINgbQlKyGi0= github.com/disintegration/imaging v1.5.0/go.mod h1:9B/deIUIrliYkyMTuXJd6OUFLcrZ2tf+3Qlwnaf/CjU= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/jackmordaunt/video-downloader v0.0.0-20181105202251-49cb2aa58132 h1:IpS/g+mhZdopN8B9PLHyqjALlOGc73RIPCIshatFSmk= -github.com/jackmordaunt/video-downloader v0.0.0-20181105202251-49cb2aa58132/go.mod h1:lpJaIhnCxJiQ14jMk0Jt0pprldUnVCliN9dUAL6Mu4M= +github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/jackmordaunt/video-downloader v0.0.0-20181105210957-4fcc0a0db5fa h1:+rrTx3ivriLXJO6Wew4qwedhW2ZbZ9YGDTuDq50hPx0= github.com/jackmordaunt/video-downloader v0.0.0-20181105210957-4fcc0a0db5fa/go.mod h1:lpJaIhnCxJiQ14jMk0Jt0pprldUnVCliN9dUAL6Mu4M= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro= github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= @@ -40,6 +39,8 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/zserge/webview v0.0.0-20181018084947-f390a2df9ec5 h1:1zYVGLwZR4gPRQdEiOBf9s63ZHGfCkQ/p99d1zHuZBQ= github.com/zserge/webview v0.0.0-20181018084947-f390a2df9ec5/go.mod h1:a1CV8KR4Dd1eP2g+mEijGOp+HKczwdKHWyx0aPHKvo4= golang.org/x/image v0.0.0-20181102021609-63626fb251ce h1:9baQ83qLsketF/x2bdSUNelOJfCgswFxV8yNnV/+6II=