After letting this one slide for quite a while, I picked it up again yesterday and have completed most of the coding. This is the full code to date (barring any further modifications found to be needed after testing).
The script is run from the directory that is to be acted upon and takes no parameters as it works by looking for all the files off the source path.
# declarationsimport glob
import argparse
import os
import shutil
import sys
import time
from PIL import Image
This is the basic declarations block. There may be some declarations that are irrelevant as it is copied from another script without checking.
# script is run from Photos subdirectory and processes subfolders
rootPath = os.getcwd()
exifDateMonth = None
exifDateYear = NonesourcePath = os.path.normpath(rootPath + “/” + “*”)
for subDir in glob.glob(sourcePath):
if os.path.isdir(subDir):
if (“_” in subDir):
In here you just have the start of things which is to look for subdirectories off the source. The subdirectories that it scans for source file must have at least one underline character in their name. This basically matches the format of directory names that Canon uses on the camera by default (generally named xxx_dd_mm although sometimes it will be xxx___mm, depending on the camera or its settings).
# process files from source directories
imagePath = os.path.normpath(subDir + “/*”)
for sourceFile in glob.glob(imagePath):
sourceExt = os.path.splitext(sourceFile)[1]
try:
image = Image.open(sourceFile)
image.verify()
except:
image = None # not an exif processable image
In this block we are getting the source file (hopefully a JPEG image with exif tags) and reading its exif data. We are using a try..except block to catch any exceptions that are raised in the process of reading the exif data block, in case there is no exif data or the source file is not an image. For example it could be a movie file which fits neither possibility.
if (image is not None):
exif = image._getexif()
try:
# get the exif tags from the image
exifDateTime = exif[36867]
exifName = exif[272]
except:
image = None
try:
exifSubSec = exif[37521]
except:
exifSubSec = “00”
This is the first of two blocks of code that only works if an Exif file has been read and the tags have been extracted from it. We are using two try..except blocks. The first is trapping the possibility that this image doesn’t have any Exif tags (which could happen with some image files) and if that happens we set the image to None (null). The second exception handling block deals with the possibility there is no SubSecTime field which could be the case with some older cameras that don’t set this field, in which case we set it to a string value of “00”. When I first started batch renaming photos off my cameras I never used to use SubSecTime and didn’t know it existed, but it has become very useful for dealing with cameras that can take multiple frames per second in order to avoid file renaming collisions. The script doesn’t have any way of detecting collisions and what the next iteration of it needs to do is to ensure where SubSecTime is not provided by a camera or is always 0, that it doesn’t overwrite an existing file of the same name (an actual collision between two different files). Because right now it will happily overwrite an existing file without even blinking.
if (image is not None):
# turn the tags into a filename
exifDateTimeParts = exifDateTime.split(” “)
exifDateParts = exifDateTimeParts[0].split(“:”)
exifDateYear = exifDateParts[0]
exifDateMonth = exifDateParts[1]
exifTimeParts = exifDateTimeParts[1].split(“:”)
exifDateStr = “”.join(exifDateParts)
exifTimeStr = “”.join(exifTimeParts) + exifSubSec
destPath = os.path.normpath(rootPath + “/” + exifDateYear + “/” + exifDateMonth)
destName = exifDateStr + ” ” + exifTimeStr + ” ” + exifName + sourceExt
destFile = os.path.normpath(destPath + “/” + destName)
print destFile
if not os.path.exists(destPath):
os.mkdir(destPath)
shutil.move(sourceFile,destFile)
This block works on valid Exif tagged files and it extracts from the date/time fields enough data to make the destination directory path. We use a path of year followed by month e.g. 2019/03 as a subdirectory of the current directory (the one from which the script is being run). We then make the new file name by combining the date, time and camera model name and the original extension. After creating the new path if it does not exist we then move the file from its original path and name to its new path and name. So the file gets renamed and moved to its new location at the same time.
if (image is None): # non exif or unreadable exif
sourceDirName = os.path.split(os.path.dirname(sourceFile))[1]
sourceDirDtParts = sourceDirName.split(“_”)[-1]
SourceDirDtMonth = sourceDirDtParts[-2:]
sourceDateTime = time.localtime(os.path.getctime(sourceFile))
sourceDateYear = sourceDateTime[0]
sourceDateMonth = SourceDirDtMonth
destName = os.path.basename(sourceFile)
destPath = os.path.normpath(rootPath + “/” + str(sourceDateYear) + “/” + str(sourceDateMonth))
destFile = os.path.normpath(destPath + “/” + destName)
print destFile
if not os.path.exists(destPath):
os.mkdir(destPath)
shutil.move(sourceFile,destFile)
The final block of code is to deal with files that don’t have Exif data in them. There is an inherent difficulty in processing these files that means this code block doesn’t rename the files, instead it leaves the file with its original name. When the files are copied off the camera onto the PC, the original timestamps are lost and replaced with new ones. The system creates the copy of the file on the PC with the current date and time as the creation time, which means we just don’t have the ability to know what the correct date to use for the destination file name is. However there is a clue in the way the camera creates the source folder with a day and month in the path. So this code basically gets the year from the file itself and then gets the month from the source directory name. It then moves the file to the new location, but without giving it a new name.
One way around this issue would be to create a script that copies directly off the camera. The problem is that the way a camera is interfaced into the operating system in Debian doesn’t create a path to access it like it was a removable hard drive. I’m not sure if taking the card out and putting it into a USB card reader will make it appear like a full file path. The script will work correctly as long as the non-image file (e.g. movie) gets copied off the camera in the same year as it was created otherwise it will end up with the wrong year.