{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreid2wkdhzveiipkxfc7xaawjrcarulkq2u2wx6a6pl7n2clhihundq",
"uri": "at://did:plc:yxqneqalyamqefe3iezpocu7/app.bsky.feed.post/3mjzkapx35ek2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreifxy2pkhfziiazibhgvts3cr35ufgzfusjldegwjhdnglgxcsexte"
},
"mimeType": "image/png",
"size": 47560
},
"description": "I have a folder named to_digikam on my Fedora computer where photos and videos land. They come from many places: Syncthing transfers from my Mac, camera offloads, or cloud exports. They arrive with generic names like IMG_0842.heicor PXL_20260421.jpg, sitting in one flat, chaotic pile until I find the time to deal with them.\n\nI wanted a workflow that would handle one tedious part of my organization workflow automatically. Specifically, a script that would:\n\n 1. Extract the YYYY-MM-DD from the act",
"path": "/a-python-script-that-sorts-my-photos-while-im-doing-something-else/",
"publishedAt": "2026-04-21T17:50:28.000Z",
"site": "https://billmoriarty.com",
"textContent": "I have a folder named `to_digikam` on my Fedora computer where photos and videos land. They come from many places: Syncthing transfers from my Mac, camera offloads, or cloud exports. They arrive with generic names like `IMG_0842.heic`or `PXL_20260421.jpg`, sitting in one flat, chaotic pile until I find the time to deal with them.\n\nI wanted a workflow that would handle one tedious part of my organization workflow automatically. Specifically, a script that would:\n\n 1. **Extract the YYYY-MM-DD** from the actual photo metadata.\n 2. **Rename the file** by prepending that date to the original filename.\n 3. **Move the file** into a `year/unsorted` directory within my main digiKam library.\n\n\n\nI’m a big fan of Derek Sivers’ approach to photo file naming: embedding context directly into the filename. Something like `2025-09-12 Sky at the beach.jpg` is more useful to me than `IMG_5502.jpg`.\n\nThe filesystem's \"modified date\" is fragile - many normal copy and sync operations reset it. The only real record is the metadata that tracks when the photo was taken. We can use **ExifTool** (the `perl-Image-ExifTool` package on Fedora), to read that internal timestamp and place it in the filename. The code then moves the file to `Pictures/photos/[YEAR]/Unsorted/`. I still keep the \"Unsorted\" folder because then I can look at it in digiKam and decide if it should be in an album or have a tag or something.\n\nPython script to sort images\n\n**The Mac-to-Fedora Handshake:**\nA cool part of this setup is the visual feedback. On my Mac, I export a photo into a synced folder. A few minutes later, the file vanishes. Because Syncthing is keeping the folders mirrored, the Fedora machine moving the file out of `to_digikam` triggers a \"delete\" on the Mac. That empty folder means Fedora handled it.\n\nFor the schedule, I went with a systemd user timer instead of a traditional cron job. On Fedora, this is the native way to handle background tasks. Unlike cron, systemd timers:\n\n * are aware of system states (if the machine is asleep, the timer can catch up immediately upon wake).\n * log directly to `journalctl` for easy troubleshooting.\n * run entirely under my user account, keeping the automation isolated from the root system.\n\n\n\nI described the logic to Gemini and it generated the Python backbone. After a few manual tweaks for my specific folder structures...success!\n\n\n import os\n import shutil\n import subprocess\n from pathlib import Path\n\n # --- CONFIGURATION ---\n # Edit these two lines for your machine.\n SOURCE_DIR = Path(\"/home/YOUR_USERNAME/path/to/intake_folder\")\n DEST_BASE = Path(\"/home/YOUR_USERNAME/Pictures/photos\")\n # ---------------------\n\n def get_date_taken(file_path):\n \"\"\"Uses exiftool to get the DateTimeOriginal metadata.\"\"\"\n try:\n cmd = [\"exiftool\", \"-DateTimeOriginal\", \"-d\", \"%Y-%m-%d %Y\", \"-S\", \"-s\", str(file_path)]\n result = subprocess.run(cmd, capture_output=True, text=True, check=True)\n if result.stdout:\n return result.stdout.strip().split(' ')\n except Exception as e:\n print(f\"Error reading metadata for {file_path}: {e}\")\n return None, None\n\n def organize_photos():\n if not SOURCE_DIR.exists():\n print(f\"Source directory {SOURCE_DIR} does not exist.\")\n return\n for file_path in SOURCE_DIR.iterdir():\n if file_path.is_dir() or file_path.name.startswith('.'):\n continue\n date_str, year_str = get_date_taken(file_path)\n if date_str and year_str:\n new_name = f\"{date_str} {file_path.name}\"\n target_dir = DEST_BASE / year_str / \"Unsorted\"\n target_path = target_dir / new_name\n target_dir.mkdir(parents=True, exist_ok=True)\n print(f\"Moving: {file_path.name} -> {target_path}\")\n shutil.move(str(file_path), str(target_path))\n else:\n print(f\"Skipping {file_path.name}: No EXIF date found.\")\n\n if __name__ == \"__main__\":\n organize_photos()\n\nI expect to edit this for edge cases and more date related naming in case that one metadata field is not always accurate, but this is a great start.",
"title": "A Python script that sorts my photos while I'm doing something else",
"updatedAt": "2026-04-24T19:44:10.884Z"
}