giffer

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

commit 15abc88f104f35f5105e26fafec5970f52f6b48f
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Tue,  6 Nov 2018 12:50:17 +1300

[+] Protoypical CLI.

Diffstat:
Acmd/cli/main.go | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acmd/webview/main.go | 5+++++
Adownload.go | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Affmpeg.go | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ago.mod | 9+++++++++
Ago.sum | 37+++++++++++++++++++++++++++++++++++++
6 files changed, 388 insertions(+), 0 deletions(-)

diff --git a/cmd/cli/main.go b/cmd/cli/main.go @@ -0,0 +1,114 @@ +package main + +import ( + "bytes" + "flag" + "image" + "image/gif" + "log" + "os" + "sync" + + "github.com/disintegration/imaging" + + "github.com/jackmordaunt/giffer" +) + +var ( + videofile string + start float64 + end float64 + dest string + fps float64 + width int + height int + url string +) + +func main() { + flag.StringVar(&videofile, "v", "", "path to video file to gifify") + flag.StringVar(&url, "url", "", "url to video file to gifenate") + flag.Float64Var(&start, "s", 0.0, "time in seconds to start the gif") + flag.Float64Var(&end, "e", 0.0, "time in seconds to end the gif") + flag.StringVar(&dest, "dest", "movie.gif", "a destination filename for the animated gif") + flag.IntVar(&width, "width", 0, "width in pixels of the output frames") + flag.IntVar(&height, "height", 0, "height in pixels of the output frames") + flag.Float64Var(&fps, "fps", 24, "frames per second") + flag.Parse() + if url != "" { + dl := giffer.Downloader{ + Dir: "./tmp/dl", + } + downloaded, err := dl.Download(url) + if err != nil { + log.Fatalf("downloading: %v", err) + } + videofile = downloaded + } + ffmpeg := giffer.FFMpeg{ + Dir: "./tmp/ffmpeg", + FPS: fps, + LeaveMess: true, + } + frames, err := ffmpeg.Extract(videofile, start, end) + if err != nil { + log.Fatalf("extracting frames: %v", err) + } + 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 + } + opfile, err := os.Create(dest) + if err != nil { + log.Fatalf("creating output file %s: %v", dest, err) + } + defer opfile.Close() + g := &gif.GIF{ + Image: paletted, + Delay: delays, + LoopCount: 0, + } + if err := gif.EncodeAll(opfile, g); err != nil { + log.Printf("encoding animated gif: %v", err) + } +} diff --git a/cmd/webview/main.go b/cmd/webview/main.go @@ -0,0 +1,5 @@ +package main + +func main() { + +} diff --git a/download.go b/download.go @@ -0,0 +1,122 @@ +package giffer + +import ( + "os" + "github.com/jackmordaunt/video-downloader/config" + "github.com/pkg/errors" + "net/url" + + "github.com/jackmordaunt/video-downloader/downloader" + "github.com/jackmordaunt/video-downloader/extractors/bcy" + "github.com/jackmordaunt/video-downloader/extractors/bilibili" + "github.com/jackmordaunt/video-downloader/extractors/douyin" + "github.com/jackmordaunt/video-downloader/extractors/douyu" + "github.com/jackmordaunt/video-downloader/extractors/facebook" + "github.com/jackmordaunt/video-downloader/extractors/instagram" + "github.com/jackmordaunt/video-downloader/extractors/iqiyi" + "github.com/jackmordaunt/video-downloader/extractors/mgtv" + "github.com/jackmordaunt/video-downloader/extractors/miaopai" + "github.com/jackmordaunt/video-downloader/extractors/pixivision" + "github.com/jackmordaunt/video-downloader/extractors/qq" + "github.com/jackmordaunt/video-downloader/extractors/tumblr" + "github.com/jackmordaunt/video-downloader/extractors/twitter" + "github.com/jackmordaunt/video-downloader/extractors/universal" + "github.com/jackmordaunt/video-downloader/extractors/vimeo" + "github.com/jackmordaunt/video-downloader/extractors/weibo" + "github.com/jackmordaunt/video-downloader/extractors/youku" + "github.com/jackmordaunt/video-downloader/extractors/youtube" + "github.com/jackmordaunt/video-downloader/utils" +) + +// Downloader is responsible for downloading videos. +type Downloader struct { + Dir string +} + +// Download the video from URL into Dir and return the full path to the +// downloaded file. +func (dl Downloader) Download(URL string) (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) +} + +// Download a video from the specified url. +func Download(videoURL string) (string, error) { + var ( + domain string + err error + data []downloader.Data + ) + bilibiliShortLink := utils.MatchOneOf(videoURL, `^(av|ep)\d+`) + if bilibiliShortLink != nil { + bilibiliURL := map[string]string{ + "av": "https://www.bilibili.com/video/", + "ep": "https://www.bilibili.com/bangumi/play/", + } + domain = "bilibili" + videoURL = bilibiliURL[bilibiliShortLink[1]] + videoURL + } else { + u, err := url.ParseRequestURI(videoURL) + if err != nil { + return "", errors.Wrap(err, "parsing uri") + } + domain = utils.Domain(u.Host) + } + switch domain { + case "douyin", "iesdouyin": + data, err = douyin.Download(videoURL) + case "bilibili": + data, err = bilibili.Download(videoURL) + case "bcy": + data, err = bcy.Download(videoURL) + case "pixivision": + data, err = pixivision.Download(videoURL) + case "youku": + data, err = youku.Download(videoURL) + case "youtube", "youtu": // youtu.be + data, err = youtube.Download(videoURL) + case "iqiyi": + data, err = iqiyi.Download(videoURL) + case "mgtv": + data, err = mgtv.Download(videoURL) + case "tumblr": + data, err = tumblr.Download(videoURL) + case "vimeo": + data, err = vimeo.Download(videoURL) + case "facebook": + data, err = facebook.Download(videoURL) + case "douyu": + data, err = douyu.Download(videoURL) + case "miaopai": + data, err = miaopai.Download(videoURL) + case "weibo": + data, err = weibo.Download(videoURL) + case "instagram": + data, err = instagram.Download(videoURL) + case "twitter": + data, err = twitter.Download(videoURL) + case "qq": + data, err = qq.Download(videoURL) + default: + data, err = universal.Download(videoURL) + } + if err != nil { + return "", errors.Wrap(err, "preparing") + } + var path string + for _, item := range data { + if item.Err != nil { + return "", errors.Wrap(item.Err, "extracting") + } + path, err = item.Download(videoURL) + if err != nil { + return "", errors.Wrap(err, "downloading") + } + } + return path, nil +} diff --git a/ffmpeg.go b/ffmpeg.go @@ -0,0 +1,101 @@ +package giffer + +import ( + "fmt" + "image" + "os" + "os/exec" + "path/filepath" + + "github.com/disintegration/imaging" + + "github.com/pkg/errors" +) + +// FFMpeg wraps the ffmpeg binary. +type FFMpeg struct { + Dir string + FPS float64 + LeaveMess bool +} + +// Extract the frames between start and end from the video file. +func (f FFMpeg) Extract(video string, start, end float64) ([]image.Image, error) { + os.RemoveAll(f.Dir) + if !f.LeaveMess { + defer os.RemoveAll(f.Dir) + } + err := os.MkdirAll(filepath.Join(f.Dir, "frames"), 0755) + if err != nil && err != os.ErrExist { + return nil, errors.Wrap(err, "preparing directories") + } + cut, err := f.Cut(video, start, end) + if err != nil { + return nil, errors.Wrap(err, "cutting video file") + } + if f.FPS == 0 { + f.FPS = 24.0 + } + // ffmpeg -i file.mp4 -r 1/1 $filename%03d.jpg + if err := f.run( + "-i", cut, + "-vf", fmt.Sprintf("fps=%2f", f.FPS), + filepath.Join(f.Dir, "frames", "$frame%03d.jpg"), + ); err != nil { + return nil, errors.Wrap(err, "extracting frames") + } + var frames []image.Image + walk := func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + frame, err := imaging.Open(path) + if err != nil { + return errors.Wrap(err, "opening frame file") + } + frames = append(frames, frame) + return nil + } + dir := filepath.Join(f.Dir, "frames") + if err := filepath.Walk(dir, walk); err != nil { + return nil, errors.Wrap(err, "walking") + } + return frames, nil +} + +// Cut the video file from start to end (in seconds). +// The returned string is the path to the resulting file. +func (f FFMpeg) Cut(video string, start, end float64) (string, error) { + // ffmpeg -ss 00:01:00 -i video.mp4 -to 00:02:00 -c copy cut.mp4 + if start > end { + return "", fmt.Errorf("start > end: %f > %f", start, end) + } + if start < 0 { + return "", fmt.Errorf("start < 0: %f < 0", start) + } + if err := f.run( + "-ss", fmt.Sprintf("%4f", start), + "-t", fmt.Sprintf("%4f", end-start), + "-i", video, + "-c", "copy", filepath.Join(f.Dir, "cut.mp4"), + ); err != nil { + return "", errors.Wrap(err, "ffmpeg") + } + return filepath.Join(f.Dir, "cut.mp4"), nil +} + +// IsInstalled checks whether FFMpeg is available on the system PATH. +func (f FFMpeg) IsInstalled() bool { + return f.run() == nil +} + +func (f FFMpeg) run(args ...string) error { + out, err := exec.Command("ffmpeg", args...).CombinedOutput() + if err != nil { + return errors.Wrap(err, string(out)) + } + return nil +} diff --git a/go.mod b/go.mod @@ -0,0 +1,9 @@ +module github.com/jackmordaunt/giffer + +require ( + github.com/disintegration/imaging v1.5.0 + github.com/jackmordaunt/video-downloader v0.0.0-20181105210957-4fcc0a0db5fa + github.com/kr/pty v1.1.3 // indirect + github.com/pkg/errors v0.8.0 + golang.org/x/image v0.0.0-20181102021609-63626fb251ce // indirect +) diff --git a/go.sum b/go.sum @@ -0,0 +1,37 @@ +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/PuerkitoBio/goquery v1.4.1 h1:smcIRGdYm/w7JSbcdeLHEMzxmsBQvl8lhf0dSw2nzMI= +github.com/PuerkitoBio/goquery v1.4.1/go.mod h1:T9ezsOHcCrDCgA8aF1Cqr3sSYbO/xgdy8/R/XiIMAhA= +github.com/andybalholm/cascadia v1.0.0 h1:hOCXnnZ5A+3eVDX8pvgl4kofXv2ELss0bKcqRySc45o= +github.com/andybalholm/cascadia v1.0.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= +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/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/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/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= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4= +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= +golang.org/x/image v0.0.0-20181102021609-63626fb251ce h1:9baQ83qLsketF/x2bdSUNelOJfCgswFxV8yNnV/+6II= +golang.org/x/image v0.0.0-20181102021609-63626fb251ce/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= +golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U= +golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +gopkg.in/cheggaaa/pb.v1 v1.0.26 h1:KbH37VyQGNNrLEz+fflXwuLLxnPNoWwUwBF783VJWUg= +gopkg.in/cheggaaa/pb.v1 v1.0.26/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw=