giffer

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

commit fb714b2b737928a2783264bcb32a9497247f1060
parent 8b556d15a6f24894ccc586fd17e2600e0deae84c
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Wed,  7 Nov 2018 12:05:57 +1300

[+] Handle both serving of UI and creating gif images.

UI can be served from a dev server (ie yarn run serve) via a reverse
proxy.

Diffstat:
Mcmd/desktop/main.go | 191++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 185 insertions(+), 6 deletions(-)

diff --git a/cmd/desktop/main.go b/cmd/desktop/main.go @@ -1,13 +1,26 @@ package main import ( + "net/http/httputil" + "net/url" + "bytes" + "encoding/json" "flag" "fmt" + "image" + "image/gif" + "io" "log" "net/http" + "path/filepath" + "strings" "sync" + "github.com/disintegration/imaging" + "github.com/jackmordaunt/giffer" + "github.com/GeertJohan/go.rice" + "github.com/pkg/errors" "github.com/gorilla/mux" @@ -16,21 +29,44 @@ import ( var ( port string + proxy string + static http.Handler // responsible for serving UI files. ) func init() { flag.StringVar(&port, "p", "8080", "port to serve on") + flag.StringVar(&proxy, "dev-proxy", "", "proxy to forward to (eg, yarn run serve)") flag.Parse() + if proxy != "" { + t, err := url.Parse(proxy) + if err != nil { + log.Fatalf("proxy: %s: not a valid URL", proxy) + } + static = &Proxy{Target: t} + } else { + static = http.FileServer(rice.MustFindBox("ui/dist").HTTPBox()) + } } func main() { ui := &UI{ + App: &Giffer{ + Downloader: &giffer.Downloader{ + Dir: "tmp/download", + }, + FFMpeg: &giffer.FFMpeg{ + Dir: "tmp/ffmpeg", + LeaveMess: true, + }, + }, Router: mux.NewRouter(), - Static: http.FileServer(rice.MustFindBox("ui").HTTPBox()), + Static: static, } svr := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: ui, + Addr: fmt.Sprintf(":%s", port), + Handler: ui, + // WriteTimeout: 15 * time.Second, + // ReadTimeout: 15 * time.Second, } go func() { if err := svr.ListenAndServe(); err != nil { @@ -52,6 +88,7 @@ func main() { // This UI is extremely simple, consisting of exactly one page, as such we refer // to that markup simply as "Template". type UI struct { + App *Giffer Router *mux.Router Static http.Handler init sync.Once @@ -65,15 +102,156 @@ func (ui *UI) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (ui *UI) routes() { - ui.Router.Handle("/", ui.serveHTML()) + // The router typically treats "/" as a unique router We have to tell + // it otherwise in order to handle file paths correctly. + ui.Router.HandleFunc("/gifify", ui.gifify()) + ui.Router.Handle("/{path:.*}", ui.Static) } -func (ui *UI) serveHTML() http.HandlerFunc { +func (ui *UI) gifify() http.HandlerFunc { + log.Printf("gifify route registered") return func(w http.ResponseWriter, r *http.Request) { - ui.Static.ServeHTTP(w, r) + log.Printf("%s /gifify", r.Method) + defer log.Printf("done") + 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"` + } + var req request + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + ui.error(w, errors.Wrap(err, "decoding json request")) + return + } + log.Printf("%#v", req) + img, err := ui.App.GififyURL( + req.URL, + req.Start, + req.End, + req.FPS, + req.Width, + req.Height) + 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, +) (*RenderedGif, error) { + videofile, err := g.Download(url) + 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: 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) +} +\ No newline at end of file