-
-
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…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
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
Also, great work!!