giffer

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

commit 12a96d1657062db54156c0647335b9c20b612c16
parent 438034ee863a3422cb08e94105c6f0585674d159
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Wed,  7 Nov 2018 16:37:41 +1300

[+] Select quality.

Diffstat:
Mcmd/cli/main.go | 2+-
Mcmd/desktop/main.go | 94+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcmd/desktop/ui/src/App.vue | 21++++++++++++++++++++-
Mdownload.go | 50+++++++++++++++++++++++++++++++++++++++++++++++---
4 files changed, 139 insertions(+), 28 deletions(-)

diff --git a/cmd/cli/main.go b/cmd/cli/main.go @@ -39,7 +39,7 @@ func main() { dl := giffer.Downloader{ Dir: "./tmp/dl", } - downloaded, err := dl.Download(url) + downloaded, err := dl.Download(url, giffer.Medium) if err != nil { log.Fatalf("downloading: %v", err) } diff --git a/cmd/desktop/main.go b/cmd/desktop/main.go @@ -1,9 +1,7 @@ package main import ( - "os" - "net/http/httputil" - "net/url" + "runtime/debug" "bytes" "encoding/json" "flag" @@ -13,6 +11,9 @@ import ( "io" "log" "net/http" + "net/http/httputil" + "net/url" + "os" "path/filepath" "strings" "sync" @@ -29,9 +30,9 @@ import ( ) var ( - port string + port string devServer string - static http.Handler // responsible for serving UI files. + static http.Handler // responsible for serving UI files. ) func init() { @@ -56,7 +57,7 @@ func main() { Dir: "tmp/download", }, FFMpeg: &giffer.FFMpeg{ - Dir: "tmp/ffmpeg", + Dir: "tmp/ffmpeg", LeaveMess: true, }, }, @@ -64,8 +65,8 @@ func main() { Static: static, } svr := &http.Server{ - Addr: fmt.Sprintf(":%s", port), - Handler: ui, + 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, @@ -104,7 +105,7 @@ func (ui *UI) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (ui *UI) routes() { log := Log{ - Logger: log.New(os.Stdout, "", 0), + Logger: log.New(LogWriteHeaderErrors{Out: os.Stdout}, "", 0), ShowBody: true, } ui.Router.Handle("/gifify", Wrap(ui.gifify(), log)) @@ -117,13 +118,14 @@ 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"` + 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 { @@ -136,7 +138,8 @@ func (ui *UI) gifify() http.HandlerFunc { req.End, req.FPS, req.Width, - req.Height) + req.Height, + giffer.Quality(req.Quality)) if err != nil { ui.error(w, err) return @@ -166,8 +169,9 @@ func (g Giffer) GififyURL( url string, start, end, fps float64, width, height int, + q giffer.Quality, ) (*RenderedGif, error) { - videofile, err := g.Download(url) + videofile, err := g.Download(url, q) if err != nil { return nil, errors.Wrap(err, "downloading") } @@ -231,7 +235,7 @@ func (g Giffer) GififyURL( r := &RenderedGif{ Buffer: buf, // Keep the title but replace the .mp4 extension with .gif - FileName: strings.Split(filepath.Base(videofile), ".")[0] + ".gif", + FileName: sanitiseFilepath(strings.Split(filepath.Base(videofile), ".")[0] + ".gif"), } return r, nil } @@ -260,7 +264,7 @@ func (p Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // Log creates middleware that logs requests. // If out is nil it is assumed that no logs are desired. type Log struct { - Logger *log.Logger + Logger *log.Logger ShowBody bool } @@ -289,10 +293,14 @@ func (l Log) Wrap(next http.Handler) http.Handler { fmt.Fprintf(msg, "Status Code: %d\n", r.Response.StatusCode) } if l.ShowBody { - by, _ := readUntil(r.Body, 1000*64) // 64Kb + 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()) @@ -319,4 +327,45 @@ func Wrap(h http.Handler, wrappers ...Wrapper) http.Handler { h = w.Wrap(h) } return h -} -\ No newline at end of file +} + +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/ui/src/App.vue b/cmd/desktop/ui/src/App.vue @@ -37,10 +37,22 @@ <input name="fps" type="number" placeholder="24" min="0" v-model="form.fps"> </span> <span class="form-group"> + <label>quality</label> + <select name="quality" v-model="form.quality"> + <option value="0">Low</option> + <option value="1">Medium</option> + <option value="2">High</option> + <option value="3">Best</option> + </select> + </span> + <span class="form-group"> <button type="submit">Submit</button> </span> </form> </div> + <div id="img" class="row"> + + </div> <div class="row"> <pre v-if="errors.length > 0" class="errors"> {{errors}} @@ -68,6 +80,7 @@ export default { width: 350, height: 0, fps: 24, + quality: 0, }, loading: false, errors: [], @@ -77,9 +90,15 @@ export default { submit() { console.log("submit") this.loading = true + this.form.quality = Number(this.form.quality) axios.post("gifify", this.form) .then(resp => { - console.log(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) }) .catch(err => { console.log(err) diff --git a/download.go b/download.go @@ -1,6 +1,7 @@ package giffer import ( + "strings" "os" "github.com/jackmordaunt/video-downloader/config" "github.com/pkg/errors" @@ -35,18 +36,55 @@ type Downloader struct { // Download the video from URL into Dir and return the full path to the // downloaded file. -func (dl Downloader) Download(URL string) (string, error) { +func (dl Downloader) Download(URL string, q Quality) (string, error) { if err := os.MkdirAll(dl.Dir, 0755); err != nil && err != os.ErrExist { return "", errors.Wrap(err, "preparing directories") } // Side channel for loading config because of how the package is // unfortunately structured. config.OutputPath = dl.Dir - return Download(URL) + return Download(URL, q) } +// Quality is an enum representing the various video qualities. +type Quality int + +// Matches returns true if the input represents the quality as a string. +// This is primarily an adaptor so we don't have to change the video-downloader +// package. +func (q Quality) Matches(str string) bool { + var patterns []string + switch q { + case Best: + patterns = append(patterns, "1080p") + case High: + patterns = append(patterns, "720p", "480p") + case Medium: + patterns = append(patterns, "360p") + case Low: + patterns = append(patterns, "240p", "144p") + } + for _, p := range patterns { + if strings.Contains(str, p) { + return true + } + } + return false +} + +const ( + // Low 144p + Low Quality = iota + // Medium 360p + Medium + // High 720p + High + // Best 1080p@60 > 720p@60 > 1080p > 720p + Best +) + // Download a video from the specified url. -func Download(videoURL string) (string, error) { +func Download(videoURL string, quality Quality) (string, error) { var ( domain string err error @@ -113,6 +151,12 @@ func Download(videoURL string) (string, error) { if item.Err != nil { return "", errors.Wrap(item.Err, "extracting") } + for k, stream := range item.Streams { + if quality.Matches(stream.Quality) { + config.Stream = k + break + } + } path, err = item.Download(videoURL) if err != nil { return "", errors.Wrap(err, "downloading")