Created
November 25, 2021 12:26
-
-
Save weliveindetail/5333dd2fc6e16f6950734ba9feb88064 to your computer and use it in GitHub Desktop.
Python3 script to rename files based on EXIF info
This file contains hidden or 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
#!/usr/local/bin/python3 | |
import os | |
from PIL import Image | |
from hachoir.parser import createParser | |
from hachoir.metadata import extractMetadata | |
# Parameters | |
dir = '/path/to/media/files' | |
image_extensions = ['.jpg'] | |
video_extensions = ['.mp4'] | |
# Processing state | |
rename_dict = {} # new_name => old_name | |
errors_dict = {} # old_name => error description | |
firsty_dict = {} # stub => new_name when no collision so far | |
suffix_dict = {} # stub => suffix index | |
try: | |
filenames = os.listdir(dir) | |
except FileNotFoundError: | |
print(f"No such file or directory: '{dir}'") | |
exit(1) | |
class UnrecognizedExtensionException(Exception): | |
pass | |
class TooManyCollisionsException(Exception): | |
pass | |
# Parse file metadata in order to find the relevant date/time info | |
def extractExifInfo(abs_name, ext): | |
# Actual date/time when the photo was taken, e.g. 2015:12:31 23:59:59 | |
if ext in image_extensions: | |
created_exif = Image.open(abs_name)._getexif()[36867] | |
return created_exif.split(' ')[0].split(':') | |
# Date/time of creation, e.g. 2015-12-31 23:59:59 | |
# FIXME: On macOS it appears to be the file's "Modified" date. | |
# How can we access the actual "Recorded" date? | |
if ext in video_extensions: | |
with createParser(abs_name) as parser: | |
metadata = extractMetadata(parser) | |
exif_dict = metadata.exportDictionary()['Metadata'] | |
return exif_dict['Creation date'].split(' ')[0].split('-') | |
raise UnrecognizedExtensionException | |
# Construct date-based filename with suffix in case of collisions | |
def constructNewFileName(year, month, day, ext): | |
# No collision, keep it simple, e.g. 2015-12-31.jpg | |
stub = f"{year}-{month}-{day}" | |
if not (stub in firsty_dict.keys() or stub in suffix_dict.keys()): | |
firsty_dict[stub] = stub + ext | |
return stub + ext | |
# Insert suffix for existing entry on first collisions, e.g. 2015-12-31a.jpg | |
if stub in firsty_dict: | |
assert(firsty_dict[stub] in rename_dict) | |
old_name = rename_dict.pop(firsty_dict.pop(stub)) | |
rename_dict[stub + 'a' + ext] = old_name | |
suffix_dict[stub] = 1 | |
# Bail out if we ran out of suffixes | |
suffix_dict[stub] += 1 | |
if suffix_dict[stub] > ord('z') - ord('a'): | |
raise TooManyCollisionsException("Last available suffix already taken: 'z'") | |
# Add the new entry with suffix, e.g. 2015-12-31b.jpg | |
suffix = chr(ord('a') + suffix_dict[stub]) | |
return stub + suffix + ext | |
# Process all files and construct new names | |
for filename in filenames: | |
try: | |
abs_name = os.path.join(dir, filename) | |
_, extension = os.path.splitext(filename) | |
year, month, day = extractExifInfo(abs_name, extension.lower()) | |
new_name = constructNewFileName(year, month, day, extension) | |
assert(not new_name in rename_dict.keys()) | |
rename_dict[new_name] = filename | |
continue | |
except KeyError: | |
errors_dict[filename] = 'Error parsing EXIF data' | |
except UnrecognizedExtensionException: | |
errors_dict[filename] = 'Unrecognized extension' | |
except TooManyCollisionsException as ex: | |
errors_dict[filename] = 'Too many collisions. ' + str(ex) | |
except Exception as ex: | |
errors_dict[filename] = 'Unknown error: ' + type(ex).__name__ | |
# Print out the mapping | |
rindent = len(max(filenames, key=len)) + 1 | |
for new_name, old_name in sorted(rename_dict.items()): | |
print(old_name.rjust(rindent) + ' => ' + new_name.ljust(35)) | |
print("\nSkipping:") | |
for old_name, err in sorted(errors_dict.items()): | |
print(old_name.rjust(rindent) + ' ' + err) | |
# Write changes to disk | |
print(f"\nAbout to rename {len(rename_dict.keys())} files") | |
if input("Proceed? (y/n) ") != 'y': | |
print("Cancelled\n") | |
exit(1) | |
for new_name, old_name in rename_dict.items(): | |
os.rename(os.path.join(dir, old_name), os.path.join(dir, new_name)) | |
print("Done\n") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment