commit 15abc88f104f35f5105e26fafec5970f52f6b48f
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Tue, 6 Nov 2018 12:50:17 +1300
[+] Protoypical CLI.
Diffstat:
| A | cmd/cli/main.go | | | 114 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | cmd/webview/main.go | | | 5 | +++++ |
| A | download.go | | | 122 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | ffmpeg.go | | | 101 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
| A | go.mod | | | 9 | +++++++++ |
| A | go.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=