Initial commit
This commit is contained in:
commit
1f3d87d37f
188
main.go
Normal file
188
main.go
Normal file
@ -0,0 +1,188 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// Standard
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
fromHour int
|
||||
toHour int
|
||||
startDir string
|
||||
outFilename string
|
||||
framerate string
|
||||
rxpFilename *regexp.Regexp
|
||||
rxpTimestamp *regexp.Regexp
|
||||
)
|
||||
|
||||
func parseCommandLine() {
|
||||
workDir, err := os.Getwd()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
flag.IntVar(&fromHour, "from", 0, "Hour each day to start picking images from")
|
||||
flag.IntVar(&toHour, "to", 24, "Hour each day to start picking images up to")
|
||||
flag.StringVar(&startDir, "dir", workDir, "Directory to find date folders in")
|
||||
flag.StringVar(&outFilename, "out", "timelapse.mp4", "path and filename for the final timelapse video")
|
||||
flag.StringVar(&framerate, "framerate", "60", "Framerate")
|
||||
flag.Parse()
|
||||
|
||||
rxpFilename = regexp.MustCompile(`(?i)^(\d+)\.(?:jpg|png)$`)
|
||||
rxpTimestamp = regexp.MustCompile(`/(\d+)\.(?:\S{3})$`)
|
||||
}
|
||||
|
||||
func findFiles(dir string) (files []string) { // {{{
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
files = append(
|
||||
files,
|
||||
findFiles(path.Join(dir, entry.Name()))...,
|
||||
)
|
||||
} else {
|
||||
if validateFilename(entry.Name()) {
|
||||
files = append(files, path.Join(dir, entry.Name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
} // }}}
|
||||
func validateFilename(fname string) bool { // {{{
|
||||
// fname is just a bunch of numbers with a .png or .jpg postfix.
|
||||
if !rxpFilename.MatchString(fname) {
|
||||
return false
|
||||
}
|
||||
|
||||
t := filenameToTime(fname, false)
|
||||
return t.Hour() >= fromHour && t.Hour() <= toHour
|
||||
} // }}}
|
||||
func filenameToTime(fname string, fnameWithPath bool) time.Time { // {{{
|
||||
var tp []string
|
||||
if fnameWithPath {
|
||||
tp = rxpTimestamp.FindStringSubmatch(fname)
|
||||
} else {
|
||||
tp = rxpFilename.FindStringSubmatch(fname)
|
||||
}
|
||||
tsi, _ := strconv.Atoi(tp[1])
|
||||
ts := int64(tsi)
|
||||
t := time.Unix(int64(ts), 0)
|
||||
sweden, _ := time.LoadLocation("Europe/Stockholm")
|
||||
t = t.In(sweden)
|
||||
return t
|
||||
} // }}}
|
||||
func sortFiles(a, b string) int { // {{{
|
||||
as := rxpTimestamp.FindStringSubmatch(a)
|
||||
bs := rxpTimestamp.FindStringSubmatch(b)
|
||||
an, _ := strconv.Atoi(as[1])
|
||||
bn, _ := strconv.Atoi(bs[1])
|
||||
switch {
|
||||
case an < bn:
|
||||
return -1
|
||||
case an > bn:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
} // }}}
|
||||
|
||||
func separateDays(files []string) *map[string][]string { // {{{
|
||||
days := make(map[string][]string, 128)
|
||||
|
||||
var t time.Time
|
||||
var date string
|
||||
var fnames []string
|
||||
var found bool
|
||||
for _, f := range files {
|
||||
t = filenameToTime(f, true)
|
||||
date = t.Format("2006-01-02")
|
||||
|
||||
fnames, found = days[date]
|
||||
if !found {
|
||||
fnames = []string{}
|
||||
}
|
||||
fnames = append(fnames, f)
|
||||
days[date] = fnames
|
||||
}
|
||||
|
||||
return &days
|
||||
} // }}}
|
||||
func filesToSequenceFile(fname string, files []string) { // {{{
|
||||
out, err := os.OpenFile(fname, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
for _, f := range files {
|
||||
_, err = out.WriteString(
|
||||
fmt.Sprintf("file '%s'\n", f),
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
} // }}}
|
||||
func timelapse(date string, files []string) { // {{{
|
||||
sequenceFile := fmt.Sprintf("/tmp/timelapse/sequence_%s", date)
|
||||
videoFile := fmt.Sprintf("/tmp/timelapse/video_%s.mp4", date)
|
||||
text := fmt.Sprintf("drawtext=text='%s':fontsize=30:x=10:y=10:fontcolor=#71b045:bordercolor=black:borderw=1", date)
|
||||
|
||||
filesToSequenceFile(sequenceFile, files)
|
||||
os.Remove(videoFile)
|
||||
ffmpeg := exec.Command("ffmpeg", "-safe", "0", "-f", "concat", "-i", sequenceFile, "-filter_complex", text, "-framerate", framerate, "-c:v", "libx264", "-crf", "23", videoFile)
|
||||
out, err := ffmpeg.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", out)
|
||||
panic(err)
|
||||
}
|
||||
} // }}}
|
||||
func concatVideos(unsortedDates []string) {
|
||||
fname := "/tmp/timelapse/sequence_all"
|
||||
var videos []string
|
||||
|
||||
for _, date := range unsortedDates {
|
||||
videos = append(videos, "/tmp/timelapse/video_"+date+".mp4")
|
||||
}
|
||||
slices.Sort(videos)
|
||||
|
||||
filesToSequenceFile(fname, videos)
|
||||
os.Remove(outFilename)
|
||||
ffmpeg := exec.Command("ffmpeg", "-safe", "0", "-f", "concat", "-i", fname, "-c", "copy", outFilename)
|
||||
out, err := ffmpeg.CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("%s\n", out)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
parseCommandLine()
|
||||
files := findFiles(startDir)
|
||||
slices.SortStableFunc(files, sortFiles)
|
||||
sortedDays := separateDays(files)
|
||||
|
||||
os.Mkdir("/tmp/timelapse", 0755)
|
||||
var dates []string
|
||||
for date, files := range *sortedDays {
|
||||
fmt.Printf("Create timelapse for %s\n", date)
|
||||
dates = append(dates, date)
|
||||
timelapse(date, files)
|
||||
}
|
||||
|
||||
fmt.Println("Assemble day videos to complete timelapse")
|
||||
concatVideos(dates)
|
||||
}
|
Loading…
Reference in New Issue
Block a user