Skip to content

Instantly share code, notes, and snippets.

@jdmonaco
Forked from cliss/organize-photos.py
Last active October 29, 2021 20:35
Show Gist options
  • Save jdmonaco/7122311 to your computer and use it in GitHub Desktop.
Save jdmonaco/7122311 to your computer and use it in GitHub Desktop.
Update of @cliss's update of Dr. Drang's photo management scripts. Replaced shutil calls with subprocess calls. Added a log file that gets written to the destination directory. Source can now be a directory tree (using os.walk). Replaced comments/docstrings with slightly better code organization and function names. Replaced path string concatena…
#!/usr/bin/env python
"""
organize-photos.py - Organize an unstructured folder tree of photos and movie
files into a month-and-year folder structure.
Note: This is a minor rewrite of @cliss's extension [1,2] of @drdrang's
photo management scripts [3], and includes a tweak from @jamiepinkham [4,5].
The lists of raw [6] and video [7] file extensions were found elsewhere.
[1] https://gist.github.com/cliss/6854904
[2] http://tumblr.caseyliss.com/day/2013/10/06
[3] http://www.leancrew.com/all-this/2013/10/photo-management-via-the-finder/
[4] https://gist.github.com/jamiepinkham/6984369
[5] https://twitter.com/drdrang/status/389952079763996672
[6] http://www.file-extensions.org/filetype/extension/name/digital-camera-raw-files
[7] http://www.fileinfo.com/filetypes/video
"""
import sys
import os, os.path
import subprocess
from datetime import datetime
## Edit these paths (relative to home) to set input and output locations
sourceDir = "Pictures/Photo Imports"
destDir = "Pictures/Photos"
## No more editing (unless you're fixing/improving the script)
JPG_EXTENSIONS = ( '.jpg', '.jpeg', '.jpe')
RAW_EXTENSIONS = ( '.3fr','.3pr','.arw','.ce1','.ce2','.cib','.cmt','.cr2','.craw','.crw',
'.dc2','.dcr','.dng','.erf','.exf','.fff','.fpx','.gray','.grey','.gry',
'.iiq','.kc2','.kdc','.mdc','.mef','.mfw','.mos','.mrw','.ndd','.nef','.nop',
'.nrw','.nwb','.orf','.pcd','.pef','.ptx','.ra2','.raf','.raw','rw2','.rwl',
'.rwz','.sd0','.sd1','.sr2','.srf','.srw','.st4','.st5','.st6','.st7','.st8',
'.stx','.x3f','.ycbcra')
PHOTO_EXTENSIONS = JPG_EXTENSIONS + RAW_EXTENSIONS
MOVIE_EXTENSIONS = ('.3g2','.3gp','.asf','.asx','.avi','.flv','.m4v','.mov','.mp4','.mpg',
'.rm','.srt','.swf','.vob','.wmv','.aepx','.ale','.avp','.avs','.bdm',
'.bik','.bin','.bsf','.camproj','.cpi','.dash','.divx','.dmsm','.dream',
'.dvdmedia','.dvr-ms','.dzm','.dzp','.edl','.f4v','.fbr','.fcproject',
'.hdmov','.imovieproj','.ism','.ismv','.m2p','.mkv','.mod','.moi',
'.mpeg','.mts','.mxf','.ogv','.otrkey','.pds','.prproj','.psh','.r3d',
'.rcproject','.rmvb','.scm','.smil','.snagproj','.sqz','.stx','.swi','.tix',
'.trp','.ts','.veg','.vf','.vro','.webm','.wlmp','.wtv','.xvid','.yuv')
SIDECAR_EXTENSIONS = ('.aae',)
VALID_EXTENSIONS = PHOTO_EXTENSIONS + MOVIE_EXTENSIONS
def get_source_date_time(f):
try:
if os.path.splitext(f)[1].lower() in MOVIE_EXTENSIONS:
raise TypeError
cDate = subprocess.check_output(['sips', '-g', 'creation', f])
cDate = cDate.split('\n')[1].lstrip().split(': ')[1]
return datetime.strptime(cDate, "%Y:%m:%d %H:%M:%S")
except:
return datetime.fromtimestamp(os.path.getmtime(f))
def get_source_filenames(d):
src = []
is_valid = lambda f: os.path.splitext(f)[1].lower() in VALID_EXTENSIONS
for dirpath, dirnames, filenames in os.walk(d):
path = os.path.join(d, dirpath)
src.extend(map(lambda f: os.path.join(path, f), filter(is_valid, filenames)))
return src
home = os.environ['HOME']
if not sourceDir.startswith(os.path.sep):
sourceDir = os.path.join(home, sourceDir)
if not destDir.startswith(os.path.sep):
destDir = os.path.join(home, destDir)
errorDir = os.path.join(destDir, 'Unsorted')
print 'Moving from %s to %s.' % (sourceDir, destDir)
sources = get_source_filenames(sourceDir)
print 'Found %d photos and videos to process.' % len(sources)
if not os.path.exists(destDir):
os.makedirs(destDir)
if not os.path.exists(errorDir):
os.makedirs(errorDir)
lastMonth = 0
lastYear = 0
fmt = "%Y-%m-%d %H-%M-%S"
problems = []
# Open a log file to record copy operations and errors
logfd = file(os.path.join(destDir, 'organize-photos.log'), 'w')
for original in sources:
suffix = 'a'
orig_base, orig_ext = os.path.splitext(original)
ext = orig_ext.lower()
if ext in JPG_EXTENSIONS:
ext = '.jpg'
sidecar = None
sidecar_ext = None
for sc_ext in SIDECAR_EXTENSIONS:
for f in (str.upper, str.lower):
sc = orig_base + f(sc_ext)
if os.path.exists(sc):
sidecar = sc
sidecar_ext = f(sc_ext)
break
try:
pDate = get_source_date_time(original)
yr = pDate.year
mo = pDate.month
if (mo, yr) != (lastMonth, lastYear):
sys.stdout.write('\nProcessing %04d-%02d...' % (yr, mo))
lastMonth = mo
lastYear = yr
elif ext in MOVIE_EXTENSIONS:
sys.stdout.write(':')
else:
sys.stdout.write('.')
newname = pDate.strftime(fmt)
thisDestDir = os.path.join(destDir, '%04d' % yr, '%02d' % mo)
if ext in MOVIE_EXTENSIONS:
thisDestDir = os.path.join(thisDestDir, 'movies')
if not os.path.exists(thisDestDir):
os.makedirs(thisDestDir)
duplicate = os.path.join(thisDestDir, newname + ext)
while os.path.exists(duplicate):
duplicate = os.path.join(thisDestDir, newname + suffix + ext)
suffix = chr(ord(suffix) + 1)
if subprocess.call(['cp', '-p', original, duplicate]) != 0:
raise Exception
print >>logfd, 'Copied: %s -> %s' % (original, duplicate)
if sidecar:
sidecar_copy = os.path.splitext(duplicate)[0] + sidecar_ext.lower()
if subprocess.call(['cp', '-p', sidecar, sidecar_copy]) != 0:
print >>logfd, 'Failed to copy sidecar: %s' % sidecar
else:
print >>logfd, 'Copied: %s -> %s' % (sidecar, sidecar_copy)
sys.stdout.flush()
except Exception:
unsorted_file = os.path.join(errorDir, os.path.basename(original))
subprocess.call(['cp', '-p', original, unsorted_file])
problems.append(original[len(home):])
print >>logfd, 'Error: unable to organize %s' % original
except:
sys.exit("Execution stopped.")
if len(problems) > 0:
print "\nProblem files:"
print "\n\t".join(problems)
print "These can be found in: %s" % errorDir
elif len(sources):
sys.stdout.write('\n')
logfd.close()
sys.exit(0)
@JMoVS
Copy link

JMoVS commented Apr 30, 2014

Also, great work!!

@jdmonaco
Copy link
Author

Sure, I just think copying is generally safer! (Also, it's not clear what people would want to do with the leftover directory tree, so it's better for the script to leave as is.) You could change Line 122 so that the call goes to the shell move command:

        if subprocess.call(['mv', original, duplicate]) != 0:
            raise Exception

        print >>logfd, 'Moved: %s -> %s' % (original, duplicate)

And thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment