Last active
December 5, 2022 12:49
-
-
Save xinau/2e16a67ad74d4d6bb8c6d427395232e5 to your computer and use it in GitHub Desktop.
Update mtime and file name of Google Photos takeout
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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