timelapse/main.go
2024-10-28 14:40:19 +01:00

179 lines
4.3 KiB
Go

package main
import (
// Standard
"bufio"
"flag"
"fmt"
"os"
"os/exec"
"path"
"regexp"
"slices"
"strconv"
"time"
)
const VERSION = "v1"
var (
version bool
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.BoolVar(&version, "version", false, "Display version and exit")
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 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 updateEXIFTimestamp(files []string) { // {{{
for _, f := range files {
t := filenameToTime(f, true)
fmt.Printf("exif for %s\n", f)
exiftool := exec.Command("exiftool", "-CreateDate="+t.Format("2006-01-02 15:04"), "-ImageDescription="+t.Format("2006-01-02 15"), "-overwrite_original", f)
exiftool.Run()
}
} // }}}
func timelapse(files []string) { // {{{
sequenceFile := fmt.Sprintf("/tmp/timelapse_sequence")
text := `drawtext=text='%{metadata\:ImageDescription}':fontsize=30:x=10:y=10:fontcolor=#71b045:bordercolor=black:borderw=1`
filesToSequenceFile(sequenceFile, files)
os.Remove(outFilename)
ffmpeg := exec.Command("ffmpeg", "-r", framerate, "-safe", "0", "-f", "concat", "-i", sequenceFile, "-filter_complex", text, "-c:v", "libx264", "-crf", "23", "-progress", "-", "-nostats", outFilename)
progress, err := ffmpeg.StdoutPipe()
if err != nil {
panic(err)
}
err = ffmpeg.Start()
if err != nil {
panic(err)
}
// Store position to overwrite progress later.
fmt.Print("\x1b[s")
rxp := regexp.MustCompile(`^frame=(\d+)$`)
ffmpegReader := bufio.NewScanner(progress)
var frameStr []string
var frame int
totalNumFrames := len(files)
for ffmpegReader.Scan() {
frameStr = rxp.FindStringSubmatch(ffmpegReader.Text())
if len(frameStr) < 2 {
continue
}
frame, _ = strconv.Atoi(frameStr[1])
fmt.Printf("\x1b[u%.0f%% ", float32(frame) / float32(totalNumFrames) * 100)
}
ffmpeg.Wait()
} // }}}
func main() {
parseCommandLine()
if version {
fmt.Println(VERSION)
return
}
files := findFiles(startDir)
slices.SortStableFunc(files, sortFiles)
timelapse(files)
}