Skip to content

Instantly share code, notes, and snippets.

@xinau
Last active December 5, 2022 12:49
Show Gist options
  • Save xinau/2e16a67ad74d4d6bb8c6d427395232e5 to your computer and use it in GitHub Desktop.
Save xinau/2e16a67ad74d4d6bb8c6d427395232e5 to your computer and use it in GitHub Desktop.
Update mtime and file name of Google Photos takeout
package main
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io/fs"
"log"
"os"
"path/filepath"
"strconv"
"strings"
"time"
)
type Metadata struct {
CreationTime CreationTimeMetadata `json:"creationTime"`
PhotoTaken PhotoTakenMetadata `json:"photoTakenTime"`
}
func (md *Metadata) Mtime() (time.Time, error) {
t := time.Time(md.PhotoTaken.Timestamp)
if t.Unix() > 0 {
return t, nil
}
t = time.Time(md.CreationTime.Timestamp)
if t.Unix() > 0 {
return t, nil
}
return time.Time{}, errors.New("timestamp smaller than unix epoch")
}
type CreationTimeMetadata struct {
Timestamp Timestamp `json:"timestamp"`
}
type PhotoTakenMetadata struct {
Timestamp Timestamp `json:"timestamp"`
}
type Timestamp time.Time
func FromUnix(sec, nsec int64) Timestamp {
t := time.Unix(sec, nsec)
return Timestamp(t)
}
func (ts *Timestamp) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
num, err := strconv.Atoi(str)
if err != nil {
return err
}
*ts = FromUnix(int64(num), 0)
return nil
}
type FileType string
var (
PhotoFileType = FileType("Photo")
VideoFileType = FileType("Video")
UnknownFileType = FileType("Unknown")
)
func FileTypeFromExt(ext string) FileType {
switch strings.ToLower(ext) {
case ".heic":
return PhotoFileType
case ".gif":
return PhotoFileType
case ".jpeg":
return PhotoFileType
case ".jpg":
return PhotoFileType
case ".mov":
return VideoFileType
case ".mp4":
return VideoFileType
case ".png":
return PhotoFileType
default:
return UnknownFileType
}
}
var renameMax = 10000
func RenameFile(out, path string, mtime time.Time) (string, error) {
ts := mtime.Format("20060102_150405")
ext := strings.ToLower(filepath.Ext(path))
ftype := FileTypeFromExt(ext)
for i := 1; i < renameMax; i++ {
file := filepath.Join(out, fmt.Sprintf("%s_%s_%04d%s", ftype, ts, i, ext))
if _, err := os.Stat(file); os.IsNotExist(err) {
return file, nil
}
}
return "", errors.New("max rename attempts reached")
}
var (
dryRunF = flag.Bool("dry-run", false, "don't perform any modifiying operations")
outDirF = flag.String("out-dir", "", "output directory for renamed files")
rootDirF = flag.String("root-dir", "", "directory root to use as start point")
renameF = flag.Bool("rename", false, "rename files acording to mask")
)
func main() {
flag.Parse()
now := time.Now()
err := filepath.Walk(*rootDirF, func(path string, info fs.FileInfo, err error) error {
if err != nil {
log.Printf("warn: walking files: %s", err)
}
if info.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".json") {
return nil
}
file := path[:len(path)-5]
if _, err := os.Stat(file); err != nil {
log.Printf("warn: no such file for metadata %s", path)
return nil
}
data, err := os.ReadFile(path)
if err != nil {
log.Printf("warn: reading file %s: %s", path, err)
return nil
}
var meta Metadata
if err := json.Unmarshal(data, &meta); err != nil {
log.Printf("warn: reading metadata of %s: %s", path, err)
return nil
}
mtime, err := meta.Mtime()
if err != nil {
log.Printf("warn: get mtime from metadata %s: %s", path, err)
return nil
}
log.Printf("info: changing time of %s to %s", file, mtime.Format(time.RFC822))
if !*dryRunF {
if err := os.Chtimes(file, now, mtime); err != nil {
log.Printf("warn: changing time of %s: %s", file, err)
return nil
}
}
if !*renameF {
return nil
}
newfile, err := RenameFile(*outDirF, file, mtime)
if err != nil {
log.Printf("warn: renaming file %s: %s", file, err)
return nil
}
log.Printf("info: renaming file %s to %s", file, newfile)
if !*dryRunF {
if err := os.Rename(file, newfile); err != nil {
log.Printf("warn: renaming file %s: %s", file, err)
return nil
}
if err := os.Remove(path); err != nil {
log.Printf("warn: removing metadata file %s: %s", path, err)
return nil
}
}
return nil
})
if err != nil {
log.Fatalf("fatal: walking dir %s: %s", *rootDirF, err)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment