Skip to content

Instantly share code, notes, and snippets.

@weliveindetail
Created November 25, 2021 12:26
Show Gist options
  • Save weliveindetail/5333dd2fc6e16f6950734ba9feb88064 to your computer and use it in GitHub Desktop.
Save weliveindetail/5333dd2fc6e16f6950734ba9feb88064 to your computer and use it in GitHub Desktop.
Python3 script to rename files based on EXIF info
#!/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