managing digital photo files via python

In the post before last I mentioned that I’d written a utility in Python to copy files off of my various camera’s SDXC-type digital cards to a location on my Macbook. Years ago I’d grown dependant on Adobe’s Lightroom to do the copying for me, keeping them stored on an external drive. When Adobe went all-in with software-as-a-service (SaaS) and replaced Lightroom 6 with its cloud equivalent, and then started charging a monthly fee to use an application I’d previously paid for just once (except for occasional updates),  I knew my days with Adobe were numbered. Unfortunately for me those days stretched into years until very recently, when I pulled the plug on Lightroom and its monthly fee. I should have done this sooner but I never seemed to find the time to Getting Around To It.

This example is meant to run on a M1 Macbook Pro with Ventura 13.5.2. I’ve tested against Python versions 3.9.6 (native to macOS), 3.10.13 and 3.11.5. The last two versions were installed via brew and inside a virtual environment for each one. You don’t have to install any other Python versions for this utility; the version native to macOS is just fine.

I’ve tried to write readable code and avoid my penchant to write complex single-line code that performs multiple functions. Yes, it’s Python, but it makes it difficult to read, I don’t care how adept you are writing Python. Anyway…

Lightroom managed all my original photo files in a directory structure that directly reflected the date the photo was taken. It was easy to navigate such a structure, even as they grew ever larger over time. The problem with that structure is that it lumped all photos taken on the same date, but with different cameras, in the same date folder. In my example I’ve actually created a directory structure that reflects all the cameras I currently own, and on occasion use:

Photography├── EM5├── EP2├── G4├── G9├── OM1M2└── PENF

When I want to save a set of photos, I plug the camera’s card into an adapter on my Macbook, then step into the directory that corresponds with the camera I’m copying from. I then run the script, which then copies all those files in, in the process creating new directories based on the camera photo file’s time stamp. This is what my Pen F folder looks like after a run.

Photography...└── PENF├── 2023_08_13├── 2023_08_15├── 2023_08_29├── 2023_08_30├── 2023_08_31├── 2023_09_02├── 2023_09_06└── 2023_09_09

Thus, if I want to see all the photos taken on 9 September 2023 then I look in the folder named 2023_09_09, and there are all the photos I took on that date using my Pen F.

On my Macbook I have the script in the Photography folder. When I want to copy photos I step into the folder corresponding to the camera I use, then execute ../copy-photos.py. The script will

  1. Check to see if a card is plugged in, discriminating between my Olympus cameras and my Panasonic cameras. If it can’t find either one it politely exits with a message.
  2. Perform a directory listing on the camera card, creating a set of folder names based on the photo files date it was taken, as well as a list of all the files.
  3. Create folders based on the unique dates it found via the photo files. If the folder already exists it won’t try to create it.
  4. Copy all the files from the card into each of the data folders. The copy also brings over each photo files metadata, so that the date the photo file was created is kept with each file, not the date it was copied over to the Macbook. If the file already exists it will not perform a new copy but skip it.

I’ve also made some minor changes on my Macbook, essentially in Finder, setting each folder holding photos to show the folder contents as a gallery. This is another essential feature that Lightroom has that is now a feature of macOS’ Finder.

Finder in gallery mode, showing all images

macOS will not only show JPEG contents, but the OS understands Olympus and Panasonic RAW file contents to properly display them in gallery mode. I have Finder to automatically associate my photo files with Affinity Photo 2. If I need to perform additional editing I can do it there, and then export to another folder. I now have every essential Lightroom feature I used, reproduced on my Macbook and macOS. As far as I’m concerned my migration away from Adobe is complete.

#!/usr/bin/env python3## Utility script to copy files from an attached digital camera# card to another location such as a folder on a Mac.#import osimport shutilfrom pathlib import Pathfrom datetime import datetime# The function converts the year/month/day portion of# a file's timestamp into a string that can be used to# create a directory, or can be used as a copy path later# when moving photos off the card and onto my system.#def create_timestamp(timestamp):d = datetime.utcfromtimestamp(timestamp)return d.strftime('%Y_%m_%d')# Set up the various global resources we'll need.## These are the known digital camera card file paths.#olym_volumn = '/Volumes/Untitled/DCIM/100OLYMP/'pana_volumn = '/Volumes/LUMIX/DCIM/100_PANA/'basepath = ''if os.path.exists(olym_volumn):basepath = olym_volumnprint('Found Olympus digital camera card.')elif os.path.exists(pana_volumn):basepath = pana_volumnprint('Found Panasonic digital camera card.')else:print("No known digital camera card found. Exiting.")exitphotopath = Path(basepath)## photo_dirs is a set. We can add many entries,# and the set functionality will make sure that# only one instance is in the set. The set# filters out duplicates.#photo_dirs = set()photos_on_card = set()# Find all the photographs on my camera's SDXC card.#for photo in photopath.iterdir():if photo.is_file():if photo.name[0] == 'P':photos_on_card.add(photo)info = photo.stat()tstamp = create_timestamp(info.st_mtime)photo_dirs.add(tstamp)# Create folders based on the unique timestamps.# Skip the make directory if the folder already exists.#for photo_dir in sorted(photo_dirs):if not os.path.exists(photo_dir):print(f'{photo_dir} - created')os.mkdir(photo_dir)else:print(f'{photo_dir} - skipped')# We now have our list of photos in the attached SDXC card.# Build full paths to copy from each photo's location on the card# to the current location where the script is executing.## If the photo on the card has already been copied, then don't copy# it again.#for photo in photos_on_card:source_photo = basepath + photo.namedestination_folder = create_timestamp(photo.stat().st_mtime)destination_target = destination_folder + '/' + photo.nameif not os.path.exists(destination_target):print(f'copy {source_photo} to {destination_target}')shutil.copy2(source_photo, destination_target)