-
-
Save slindner05/377e787a88c38df5ab59bd23952cd402 to your computer and use it in GitHub Desktop.
Photo management script. This script will copy photos from "~/Pictures/iPhone Incoming" into a tree the script creates, with folders representing month and years, and photo names timestamped.Completely based on the work of the amazing Dr. Drang; see here: http://www.leancrew.com/all-this/2013/10/photo-management-via-the-finder/You can see more a…
This file contains hidden or 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/python | |
import sys | |
import os | |
import subprocess | |
import shlex | |
import os.path | |
from datetime import datetime | |
from dateutil import tz | |
from dateutil.parser import parse | |
import argparse | |
######################## Functions ######################### | |
fileTypeGroupings = {'photos': ['jpg', 'jpeg', 'jpe', 'png', 'bmp', 'raw'], | |
'movies': ['.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'], | |
'audio': ['mp3', 'wav']} | |
inverted = {} | |
for group in fileTypeGroupings: | |
for extension in fileTypeGroupings[group]: | |
if inverted.setdefault(extension, group) != group: | |
raise Exception('duplicate shit for %s' % extension) | |
def fileDate(f): | |
"Return the date/time on which the given file was created" | |
try: | |
# cDate = subprocess.check_output(['sips', '-g', 'creation', f]) | |
# cDate = cDate.split('\n')[1].lstrip().split(': ')[1] | |
# cDate = subprocess.check_output(['mdls', f, '|', 'grep', 'kMDItemFSCreationDate']) | |
# proc1 = subprocess.Popen(['mdls', '-name', 'kMDItemContentCreationDate', f], stdout=subprocess.PIPE,stderr=subprocess.PIPE) | |
proc1 = subprocess.Popen(['exiftool', '-CreateDate', f], stdout=subprocess.PIPE,stderr=subprocess.PIPE) | |
# | |
# proc2 = subprocess.Popen(['grep','kMDItemFSCreationDate'],stdin=proc1.stdout, | |
# stdout=subprocess.PIPE,stderr=subprocess.PIPE) | |
# proc1.stdout.close() | |
out,err = proc1.communicate() | |
print '"%s"' % out | |
cDate = out.split('\n')[0].lstrip().split(': ')[1] | |
theDate = datetime.strptime(cDate, "%Y:%m:%d %H:%M:%S") #parse(cDate).astimezone(tz.tzlocal()) | |
except Exception as e: | |
# fall back to sips | |
print 'failed to use exif tool... falling back to sips' | |
try: | |
cDate = subprocess.check_output(['sips', '-g', 'creation', f]) | |
cDate = cDate.split('\n')[1].lstrip().split(': ')[1] | |
theDate = datetime.strptime(cDate, "%Y:%m:%d %H:%M:%S") | |
except Exception as ee: | |
# worst case use mdls | |
print 'failed to use sips... falling back to mdls' | |
try: | |
proc1 = subprocess.Popen(['mdls', '-name', 'kMDItemContentCreationDate', f], stdout=subprocess.PIPE,stderr=subprocess.PIPE) | |
out,err = proc1.communicate() | |
print '"%s"' % out | |
cDate = out.split('\n')[0].lstrip().split('= ')[1] | |
theDate = parse(cDate).astimezone(tz.tzlocal()) | |
except Exception as eee: | |
print 'failed to use mdls... raising exception' | |
# fallback to modified time??? | |
# return datetime.fromtimestamp(os.path.getmtime(f)) | |
raise | |
return theDate | |
def filesHaveDifferentMd5s(file, duplicate): | |
md5orig = getMd5(file) | |
md5dup = getMd5(duplicate) | |
return md5orig != md5dup | |
md5cache = {} | |
def getMd5(file): | |
if not file in md5cache: | |
proc1 = subprocess.Popen(['md5', file], stdout=subprocess.PIPE,stderr=subprocess.PIPE) | |
out,err = proc1.communicate() | |
md5 = out.split('\n')[0].lstrip().split('= ')[1] | |
md5cache[file] = md5 | |
return md5cache[file] | |
def make_parser(): | |
""" Creates an ArgumentParser to parse the command line options. """ | |
parser = argparse.ArgumentParser(description='Rename photos to names based on when they were taken.') | |
parser.add_argument('--srcdir', help='source directory.', default=os.environ['HOME'] + '/Pictures/iPhone Incoming') | |
parser.add_argument('--destdir', help='destination directory.', default=os.environ['HOME'] + '/Pictures/iPhone') | |
parser.add_argument('--format', help='The format for the new file names', default='%Y-%m-%d %H-%M-%S') | |
return parser | |
###################### Main program ######################## | |
parser = make_parser() | |
arguments = parser.parse_args() | |
sourceDir = arguments.srcdir | |
destDir = arguments.destdir | |
errorDir = destDir + '/Unsorted/' | |
fmt = arguments.format | |
# Where the files are and where they're going. | |
#sourceDir = os.environ['HOME'] + '/Desktop/synology/Unsorted' | |
#sourceDir = '/Volumes/media/Pictures/' | |
#sourceDir = '/Users/lindner/Desktop/synology' | |
#sourceDir = '/Volumes/Macintosh HD/Users/lindner/Pictures/Photos Library.photoslibrary/Originals/' | |
#destDir = os.environ['HOME'] + '/Desktop/synology/renamed' | |
#destDir = '/Users/lindner/Desktop/photos renamed exif' | |
#destDir = '/Volumes/media/iPhoto/copy_from_script' | |
#sourceDir = os.environ['HOME'] + '/foo/bar' | |
#destDir = os.environ['HOME'] + '/zed/blah' | |
#errorDir = destDir + '/Unsorted2/' | |
# The format for the new file names. | |
#fmt = "%Y-%m-%d %H-%M-%S" | |
# The problem files. | |
problems = [] | |
md5duplicates = [] | |
# Get all the files in the source folder - include sub directories | |
file_list = [] | |
for root, dirs, files in os.walk(sourceDir, topdown=False): | |
for name in files: | |
if not name.endswith('.DS_Store'): # should I exclude other unknown extensions? or just organize everything? | |
file_list.append(os.path.join(root, name)) | |
# Prepare to output as processing occurs | |
lastMonth = 0 | |
lastYear = 0 | |
# Create the destination folder if necessary | |
if not os.path.exists(destDir): | |
os.makedirs(destDir) | |
if not os.path.exists(errorDir): | |
os.makedirs(errorDir) | |
# Copy files into year and month subfolders. Name the copies according to | |
# their timestamps. If more than one file has the same timestamp, add | |
# suffixes 'a', 'b', etc. to the names. | |
for file in file_list: | |
# print "Processing %s..." % file | |
original = file | |
filename, extension_with_dot = os.path.splitext(original) | |
lowercase_ext = extension_with_dot.lower() | |
no_dot_ext = lowercase_ext.rpartition(".")[-1] | |
if no_dot_ext == '': | |
no_dot_ext = 'no_extension' | |
suffix = 'a' | |
try: | |
fDate = fileDate(original) | |
yr = fDate.year | |
mo = fDate.month | |
if not lastYear == yr or not lastMonth == mo: | |
sys.stdout.write('\nProcessing %04d-%02d...' % (yr, mo)) | |
lastMonth = mo | |
lastYear = yr | |
else: | |
sys.stdout.write('.') | |
newname = fDate.strftime(fmt) | |
if no_dot_ext in inverted: | |
group = inverted[no_dot_ext] | |
else: | |
group = no_dot_ext | |
thisDestDir = destDir + '/%s/%04d/%02d' % (group, yr, mo) | |
if not os.path.exists(thisDestDir): | |
os.makedirs(thisDestDir) | |
duplicate = thisDestDir + '/%s%s' % (newname,extension_with_dot) | |
md5dup = False | |
while os.path.exists(duplicate): | |
if not filesHaveDifferentMd5s(file, duplicate): | |
md5dup = True | |
break; | |
newname = fDate.strftime(fmt) + suffix | |
duplicate = destDir + '/%s/%04d/%02d/%s%s' % (group,yr, mo, newname,extension_with_dot) | |
suffix = chr(ord(suffix) + 1) | |
if not md5dup: | |
if subprocess.call(['cp', '-p', original, duplicate]) != 0: | |
raise Exception("failed to copy files") | |
else: | |
print "MD5 DUP!!!" | |
md5duplicates.append(file) | |
except Exception as e: | |
print '%s --> %s' % (e, file) | |
final_dir = '%s/%s/' % (errorDir, no_dot_ext) | |
if not os.path.exists(final_dir): | |
os.makedirs(final_dir) | |
subprocess.call(['cp', '-p', original, final_dir]) | |
problems.append(file) | |
except: | |
sys.exit("Execution stopped.") | |
# Report the problem files, if any. | |
if len(problems) > 0: | |
print "\nProblem files: %s" % len(problems) | |
print "\n".join(problems) | |
print "These can be found in: %s" % errorDir | |
if len(md5duplicates) > 0: | |
print "\nMD5 Duplicate files: %s" % len(md5duplicates) | |
print "\n".join(md5duplicates) | |
print "These files were not copied" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment