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)
@Cantarione
Copy link

Good fork!

Can it be modified to take into account to manage also the movies captured by the mobile phone?

@jdmonaco
Copy link
Author

jdmonaco commented Feb 4, 2014

Absolutely! That was actually the next thing I planned to do, but I hadn't decided how I wanted to deal with movies yet. Basically the extension check on Line 36 should be generalized to movies, and then the move file extensions need to persist until the destination (duplicate) filenames are created on Lines 84 and 86. Right now everything is clobbered into a standard '.jpg', which obviously won't work for movie files.

(Hmm, it would help to get notifications on comments on gists, sorry about that delay!)

@jdmonaco
Copy link
Author

jdmonaco commented Apr 7, 2014

The latest revision now organizes RAW camera files in addition to JPGs. Movie files are now handled as well, they are copied into the same year/month-year directory tree but collected under "movies/" subfolders.

@JMoVS
Copy link

JMoVS commented Apr 30, 2014

Hi, would it be feasable to modify the script to move instead of copy the files? (or delete successfully copied files the moment they are copied)?

@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