From a890c9141c98c9370d6de2c2d952d93b51dd7c8a Mon Sep 17 00:00:00 2001 From: Pablu23 Date: Tue, 11 Jun 2024 14:43:08 +0200 Subject: [PATCH] Initial commit working --- .gitignore | 1 + cmd/thumbnail-gen/main.go | 35 ++++++++ generator.go | 173 ++++++++++++++++++++++++++++++++++++++ go.mod | 3 + 4 files changed, 212 insertions(+) create mode 100644 .gitignore create mode 100644 cmd/thumbnail-gen/main.go create mode 100644 generator.go create mode 100644 go.mod diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e33609d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.png diff --git a/cmd/thumbnail-gen/main.go b/cmd/thumbnail-gen/main.go new file mode 100644 index 0000000..e80105e --- /dev/null +++ b/cmd/thumbnail-gen/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + thumbnailgen "github.com/pablu23/thumbnail-gen" +) + +var ( + pathFlag = flag.String("path", "", "path to video") + intervalFlag = flag.Int("interval", 20, "Interval in seconds between thumbnails") + maxThumbnailsFlag = flag.Int("max", 0, "Max Thumbnails, default as much as possible with interval") +) + +func main() { + flag.Parse() + + if *pathFlag != "" { + thumbnails, err := thumbnailgen.GetThumbnail(*pathFlag, *intervalFlag, *maxThumbnailsFlag) + if err != nil { + panic(err) + } + + name := filepath.Base(*pathFlag) + for i, thumbnail := range thumbnails { + err := os.WriteFile(fmt.Sprintf("%s-%d.png", name, i), thumbnail, 0600) + if err != nil { + panic(err) + } + } + } +} diff --git a/generator.go b/generator.go new file mode 100644 index 0000000..a80a64b --- /dev/null +++ b/generator.go @@ -0,0 +1,173 @@ +package thumbnailgen + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "slices" + "strconv" + "strings" + "time" +) + +type TimeFilter struct { + Start float64 + End float64 +} + +func GetThumbnail(path string, intervalSeconds int, maxThumbnails int) ([][]byte, error) { + filters, err := GetFilter(path) + if err != nil { + return nil, err + } + + fps, err := GetFramerate(path) + if err != nil { + return nil, err + } + + length, err := GetVideoLength(path) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(nil) + currFrame := 0 + framesExtracted := 0 + + var out [][]byte + if maxThumbnails > 0 { + out = make([][]byte, maxThumbnails) + } else { + out = make([][]byte, 0) + } + + for { + time := float64(currFrame) / fps + if (maxThumbnails > 0 && framesExtracted >= maxThumbnails) || time >= length { + break + } + + currFrame += int(fps) * intervalSeconds + if FrameLiesWithinFilter(time, filters) { + continue + } + + err := GetImage(buf, path, int(time), "png") + if err != nil { + return nil, err + } + + b := bytes.Clone(buf.Bytes())[0:buf.Len()] + + if maxThumbnails > 0 { + out[framesExtracted] = b + } else { + out = append(out, b) + } + + framesExtracted += 1 + buf.Reset() + } + return out, nil +} + +func FrameLiesWithinFilter(time float64, filters []TimeFilter) bool { + for _, filter := range filters { + if time >= filter.Start && time <= filter.End { + return true + } + + } + return false +} + +func GetImage(buf *bytes.Buffer, path string, timestamp int, format string) error { + var t time.Time + t = t.Add(time.Duration(timestamp) * time.Second) + cmd := exec.Command("ffmpeg", "-ss", t.Format("15:04:05"), "-i", path, "-vframes", "1", "-c:v", format, "-f", "image2pipe", "-") + cmd.Stdout = buf + err := cmd.Run() + return err +} + +func GetVideoLength(path string) (float64, error) { + cmd := exec.Command("ffprobe", "-v", "quiet", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", path) + buf := bytes.NewBuffer(nil) + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + return 0, err + } + + return strconv.ParseFloat(strings.ReplaceAll(buf.String(), "\n", ""), 64) +} + +func GetFilter(path string) ([]TimeFilter, error) { + buf := bytes.NewBuffer(nil) + cmd := exec.Command("ffprobe", "-f", "lavfi", "-i", fmt.Sprintf("movie=%s,blackdetect[out0]", path), "-show_entries", "tags=lavfi.black_start,lavfi.black_end", "-of", "default=nw=1", "-v", "quiet") + cmd.Stdout = buf + err := cmd.Run() + // TODO: Replace with strings.Builder + filterStr := buf.String() + filters := strings.Split(filterStr, "\n") + filters = slices.Compact(filters) + i := 0 + reg := regexp.MustCompile(`([0-9]*\.?[0-9]+)`) + blackFilters := make([]TimeFilter, len(filters)/2) + for { + if i >= len(filters) { + break + } + start := reg.FindString(filters[i]) + var end string + if i+1 >= len(filters) || filters[i+1] == "" { + end = start + } else { + end = reg.FindString(filters[i+1]) + } + + s, err := strconv.ParseFloat(start, 64) + if err != nil { + return nil, err + } + + e, err := strconv.ParseFloat(end, 64) + if err != nil { + return nil, err + } + + blackFilters[i/2] = TimeFilter{ + Start: s, + End: e, + } + i += 2 + } + + return blackFilters, err +} + +func GetFramerate(path string) (float64, error) { + cmd := exec.Command("ffprobe", "-v", "0", "-of", "csv=p=0", "-select_streams", "v:0", "-show_entries", "stream=r_frame_rate", path) + buf := bytes.NewBuffer(nil) + cmd.Stdout = buf + err := cmd.Run() + if err != nil { + return 0, err + } + s := buf.String() + s = strings.ReplaceAll(s, "\n", "") + s = strings.TrimSpace(s) + ss := strings.Split(s, "/") + f1, err := strconv.Atoi(ss[0]) + if err != nil { + return 0, err + } + + f2, err := strconv.Atoi(ss[1]) + if err != nil { + return 0, err + } + return float64(f1) / float64(f2), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8a328bc --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/pablu23/thumbnail-gen + +go 1.22.4