]> code.delx.au - monosys/blob - hacks/rename-by-date
Rename all the things
[monosys] / hacks / rename-by-date
1 #!/usr/bin/python3
2
3 import argparse
4 import datetime
5 import os
6 import subprocess
7 import sys
8
9
10 def read_directories(src_directories):
11 result = []
12 for src_directory in src_directories:
13 for root, dirnames, filenames in os.walk(src_directory):
14 for filename in filenames:
15 filename = os.path.join(root, filename)
16 result.append(filename)
17 result.sort()
18 return result
19
20 def get_timestamp(filename):
21 ext = os.path.splitext(filename.lower())[1]
22
23 if ext == ".jpg":
24 return get_jpg_timestamp(filename)
25
26 if ext == ".mp4":
27 return get_mp4_timestamp(filename)
28
29 raise NotImplementedError("Unsupported extension: " + ext)
30
31 def get_mp4_timestamp(filename):
32 output = subprocess.check_output([
33 "ffprobe", filename, "-show_format", "-v", "quiet",
34 ]).decode("utf-8")
35 line = [line for line in output.split("\n") if line.startswith("TAG:creation_time=")][0]
36 value = line.split("=")[1].split(".")[0]
37 return datetime.datetime.strptime(value, "%Y-%m-%dT%H:%M:%S")
38
39 def get_jpg_timestamp(filename):
40 output = subprocess.check_output([
41 "exiv2", "-pt", "-g", "Exif.Photo.DateTimeOriginal", filename
42 ]).decode("utf-8")
43 first_line = output.split("\n")[0]
44 timestamp = " ".join(first_line.split()[-2:])
45 return datetime.datetime.strptime(timestamp, "%Y:%m:%d %H:%M:%S")
46
47 class FilesByDate(object):
48 def __init__(self, dest, src_directories):
49 self.dest = dest
50 self.src_directories = src_directories
51
52 def plan(self):
53 sorted_filenames = self.get_sorted_filenames()
54
55 for i, filename in enumerate(sorted_filenames, 1):
56 yield filename, self.get_new_filename(filename, i)
57
58 def get_sorted_filenames(self):
59 src_filenames = read_directories(self.src_directories)
60
61 ts_filenames = []
62 for filename in src_filenames:
63 timestamp = get_timestamp(filename)
64 ts_filenames.append((timestamp, filename))
65 ts_filenames.sort()
66
67 return [filename for _, filename in sorted(ts_filenames)]
68
69 def get_new_filename(self, orig_filename, i):
70 prefix = "S" + str(i).zfill(3) + "_"
71 orig_filename = os.path.basename(orig_filename)
72 return os.path.join(self.dest, prefix + orig_filename)
73
74 class DirectoryFanout(object):
75 def __init__(self, dest, src_directories):
76 self.dest = dest
77 self.src_directories = src_directories
78
79 def plan(self):
80 src_filenames = read_directories(self.src_directories)
81
82 for filename in src_filenames:
83 timestamp = get_timestamp(filename)
84 yield filename, self.get_new_filename(filename, timestamp)
85
86 def get_new_filename(self, orig_filename, timestamp):
87 prefix = timestamp.strftime("%Y-%m-%d") + "/"
88 orig_filename = os.path.basename(orig_filename)
89 return os.path.join(self.dest, prefix + orig_filename)
90
91 def print_plan(plan):
92 for orig_filename, new_filename in plan:
93 print(" ", orig_filename, "->", new_filename)
94
95 def execute_plan(plan):
96 for orig_filename, new_filename in plan:
97 os.makedirs(os.path.dirname(new_filename), exist_ok=True)
98 os.link(orig_filename, new_filename)
99
100 def parse_args():
101 parser = argparse.ArgumentParser(description="Relink photos based on EXIF dates")
102
103 parser.add_argument("--dry-run", action="store_true")
104
105 parser.add_argument("dest", nargs=1)
106 parser.add_argument("src", nargs="+")
107
108 action_group = parser.add_mutually_exclusive_group(required=True)
109
110 action_group.add_argument("--directory-fanout", action="store_true",
111 help="Create directories with names like 2015-01-01, place files into them")
112
113 action_group.add_argument("--rename-files", action="store_true",
114 help="Rename files from different cameras based on timestamps")
115
116 args = parser.parse_args()
117 args.dest = args.dest[0]
118 return args
119
120 def main():
121 args = parse_args()
122
123 def planner():
124 raise NotImplementedError()
125
126 if args.directory_fanout:
127 planner = DirectoryFanout(args.dest, args.src).plan
128 elif args.rename_files:
129 planner = FilesByDate(args.dest, args.src).plan
130
131 plan = planner()
132 if args.dry_run:
133 print_plan(plan)
134 else:
135 execute_plan(plan)
136
137 if __name__ == "__main__":
138 main()
139