179 lines
4.3 KiB
Go
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)
|
|
}
|