-
-
Save jdmonaco/7122311 to your computer and use it in GitHub Desktop.
#!/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) |
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!)
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.
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)?
Also, great work!!
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!
Good fork!
Can it be modified to take into account to manage also the movies captured by the mobile phone?