# flickruploadr.py # By Chad Cooper | http://www.super-cooper.com | supercooper@gmail.com # Based on and most code from uploadr.py by http://berserk.org/uploadr/ # Most of this code is from uploadr.py, I just hacked it to do what I # wanted and added a few functions to rotate photos and add them to sets # # Uploadr.py inpired by http://micampe.it/projects/flickruploadr import sys, time, os, urllib2, shelve, string, xmltramp, mimetools import mimetypes, md5, webbrowser from IPTC import IPTCInfo #from mx import DateTime import EXIF, traceback, urllib # Location to scan for new images # Grab todays date info td = time.strftime('%m/%d/%Y', time.localtime()) tdY = td.split('/')[2] tdM = td.split('/')[1] tdD = td.split('/')[0] # Calc how far to go back and search for images # Lets do 2 months #back = DateTime.RelativeDateTime(years=0,months=0,days=10) # Begin date #bd = DateTime.now() - back # Parse beginDate #bdY = bd[0].split('-')[0] # Year #bdM = bd[0].split('-')[1] # Month #bdD = bd[0].split('-')[2] # Date #IMAGE_DIR = "/Users/chad/Pictures/" + str(tdY) + "/" IMAGE_DIR = "/Users/chad/Pictures/temp/" # Flickr settings FLICKR = {"title": "", "description": "", "tags": "auto-uploaded"} # How often to check for new images to upload (in seconds ) SLEEP_TIME = 1 * 60 # File we keep the history of uploaded images in. HISTORY_FILE = "uploadr.history" # Flickr API key and secret FLICKR["secret" ] = "your-secret" FLICKR["api_key" ] = "your-api-key" class APIConstants: base = "http://flickr.com/services/" rest = base + "rest/" auth = base + "auth/" upload = base + "upload/" token = "auth_token" secret = "secret" key = "api_key" sig = "api_sig" frob = "frob" perms = "perms" method = "method" photo_id = 'photo_id' photoset_id = 'photoset_id' def __init__( self ): pass api = APIConstants() class Uploadr: token = None perms = "" TOKEN_FILE = ".flickrToken" def __init__( self ): self.token = self.getCachedToken() def signCall( self, data): """ Signs args via md5 per http://www.flickr.com/services/api/auth.spec.html (Section 8) """ keys = data.keys() keys.sort() foo = "" for a in keys: foo += (a + data[a]) f = FLICKR[ api.secret ] + api.key + FLICKR[ api.key ] + foo #f = api.key + FLICKR[ api.key ] + foo return md5.new( f ).hexdigest() def urlGen( self , base,data, sig ): foo = base + "?" for d in data: foo += d + "=" + data[d] + "&" return foo + api.key + "=" + FLICKR[ api.key ] + "&" + api.sig + "=" + sig def authenticate( self ): """ Authenticate user so we can upload images """ print "Getting new Token" self.getFrob() self.getAuthKey() self.getToken() self.cacheToken() def getFrob( self ): """ Returns a frob to be used during authentication. This method call must be signed. """ d = { api.method : "flickr.auth.getFrob" } sig = self.signCall( d ) url = self.urlGen( api.rest, d, sig ) try: response = self.getResponse( url ) if ( self.isGood( response ) ): FLICKR[ api.frob ] = str(response.frob) else: self.reportError( response ) except: print "Error getting frob:" , str( sys.exc_info() ) def getAuthKey( self ): """ Checks to see if the user has authenticated this application """ d = { api.frob : FLICKR[ api.frob ], api.perms : "write" } sig = self.signCall( d ) url = self.urlGen( api.auth, d, sig ) ans = "" try: webbrowser.open( url ) ans = raw_input("Have you authenticated this application? (Y/N): ") except: print str(sys.exc_info()) if ( ans.lower() == "n" ): print "You need to allow this program to access your Flickr site." print "A web browser should pop open with instructions." print "After you have allowed access restart uploadr.py" sys.exit() def getToken( self ): """ Returns the auth token for the given frob, if one has been attached. This method call must be signed. """ d = { api.method : "flickr.auth.getToken", api.frob : str(FLICKR[ api.frob ]) } sig = self.signCall( d ) url = self.urlGen( api.rest, d, sig ) try: res = self.getResponse( url ) if ( self.isGood( res ) ): self.token = str(res.auth.token) self.perms = str(res.auth.perms) self.cacheToken() else : self.reportError( res ) except: print str( sys.exc_info() ) def getCachedToken( self ): """ Attempts to get the flickr token from disk. """ if ( os.path.exists( self.TOKEN_FILE )): return open( self.TOKEN_FILE ).read() else : return None def cacheToken( self ): try: open( self.TOKEN_FILE , "w").write( str(self.token) ) except: print "Issue writing token to local cache " , str(sys.exc_info()) def checkToken( self ): """ Returns the credentials attached to an authentication token. """ if ( self.token == None ): return False else : d = { api.token : str(self.token) , api.method : "flickr.auth.checkToken" } sig = self.signCall( d ) url = self.urlGen( api.rest, d, sig ) try: res = self.getResponse( url ) if ( self.isGood( res ) ): self.token = res.auth.token self.perms = res.auth.perms return True else : self.reportError( res ) except: print str( sys.exc_info() ) return False def determinePrivacy( self, image ): """ Checks image IPTC metadata tags to see if pic is tagged with family member names. If so, pic privacy is flagged as 'is_family'; otherwise, privacy is 'public' """ info = IPTCInfo(image) iptcKeys = info.keywords # List of people we want photos of to be private family = ['will','harrison','adria','terri','chad','mom','dad', 'lee','caro','carolyn','adi','ken','jacob','angelina', 'charlie','harri'] for person in family: # If person is in the photo, flag as family and move on (break) if person in iptcKeys: privacy = 'is_family' break else: privacy = 'is_public' return privacy def determineRotation( self, image ): """ Reads the EXIF tags and determines if the image had been auto- rotated. If it has, the numeric value is returned. If not, then zero is returned. """ im = open(image,'rb') f = EXIF.process_file(im) imOrient = str(f['Image Orientation']) if imOrient == 'Rotated 90 CW': deg = 90 elif imOrient == 'Rotated 90 CCW': deg = 270 else: deg = 0 return deg def determineSetMembership( self, image ): """ Based on presence of certain IPTC keywords, determines whether or not image needs to be added to a set """ # Grab keywords for IPTC info = IPTCInfo(image) iptcKeys = info.keywords # Only interested in adding pix of the boys to their respective # sets. If both boys are in pix, add pix to the 'Brothers' set boys = ['will','harrison'] # New empty list object sm = [] # Determine which set(s) the photo belongs in; add the set id to # to our set list if boys[0] in iptcKeys: sm.append('381732') # will set if boys [1] in iptcKeys: sm.append('72057594052259748') # bros set if boys[1] in iptcKeys: sm.append('817740') # harri set if boys[0] in iptcKeys: sm.append('72057594052259748') # bros set # Convert sm to a set to get rid of possible dups, then back to list lsms = list(set(sm)) # Return the sets as a list, if any return lsms def rotatePhoto( self, photo_id, rdegrees, rimage ): """ Thru a URL POST call, rotates a photo by rdegrees. Most images from my D70 that need it get rotated 270 CW, some get rotated 90 CW. """ try: # Build the method call url in a dictionary rd = {api.method : 'flickr.photos.transform.rotate', 'photo_id' : str(photo_id), 'degrees' : str(rdegrees), api.token : str(self.token)} # Sign the api call rsig = self.signCall( rd ) # Add the signature and api key to dictionary object rd[ api.sig ] = rsig rd[ api.key ] = FLICKR[ api.key ] # Encode the dictionary keys/values into a url string params = urllib.urlencode(rd) # Execute the POST call roturl = urllib.urlopen(api.rest, params) del rsig del rd except: traceback.print_exc() def grabIptcKeywords( self, image ): """ Get the IPTC keywords for a image as a list """ info = IPTCInfo(image) iptcKeywords = info.keywords return iptcKeywords def grabExifTags( self, image ): """ Gets EXIF tags for image """ im = open(image,'rb') exifTags = EXIF.process_file(im) return exifTags def upload( self ): """ Iterates thru list of images, pulls IPTC tags for image, determines privacy and rotation, and feeds it all into uploadImage function """ newImages = self.grabNewImages() if ( not self.checkToken() ): self.authenticate() self.uploaded = shelve.open( HISTORY_FILE ) for image in newImages: # TODO - call grabIptcKeywords and grabExifTags and pass # their returns into the determinePrivacy, determineRotation, # uploadImage, and addToSet functions - will save several # IPTC method calls and make things cleaner info = IPTCInfo(image) #info = self.grabIptcKeywords( image ) if len(info.data) < 4: raise Exception(info.error) # Determine privacy level privLevel = self.determinePrivacy( image ) # Determine rotation of photo rot = self.determineRotation( image ) # Determine if the photo needs to be added to an existing set st = self.determineSetMembership( image ) # Call uploadImage self.uploadImage( image, privLevel, rot , st) del info del privLevel del rot self.uploaded.close() def grabNewImages( self ): """ Recurses thru directories and looks for images to upload. I only want to upload my '5-star' images, so we scan the IPTC tags for 'r5', my way of tagging my pics I really like (usually get prints of r5s) """ images = [] for dirpath, dirnames, filenames in os.walk(IMAGE_DIR): for f in filenames : # Grab IPTC keywords info = IPTCInfo(os.path.join(dirpath, f)) # Is it a 5-star photo? if 'r5' in info.keywords: print 'File to upload:',os.path.join(dirpath, f) ext = f.lower().split(".")[-1] if ( ext == "jpg" or ext == "gif" or ext == "png" ): images.append( os.path.normpath( dirpath + "/" + f ) ) images.sort() return images def getRotationTag( self, rotationDegrees ): """ Based on the rotation, in degrees, derive a tag to be added to the string of space-separated tag values for flickr upload """ rDeg = str(rotationDegrees) if rDeg == '90': rt = 'auto-rotated-90' elif rDeg == '180': rt = 'auto-rotated-180' elif rDeg == '270': rt = 'auto-rotated-270' else: rt = ' ' return rt def uploadImage( self, image, priv, rotation, st ): """ Upload our image. While we're at it, add some tags for auto-upload and auto-rotation (if done). If the photo needs rotated, rotate it based on the auto-orientation tag found in Nikon EXIF tags """ if ( not self.uploaded.has_key( image ) ): print "Uploading ", image , "...", try: # If rotated, get the tag rotTag = self.getRotationTag( rotation ) if len(str(rotTag > 3)): tagStr = str(FLICKR['tags']) + ' ' + rotTag else: tagStr = str(FLICKR['tags']) photo = ('photo', image, open(image,'rb').read()) # Build method call in dictionary d = {api.token : str(self.token), api.perms : str(self.perms), 'tags' : str(tagStr)} # Determine privacy level from input, add proper key/value # pairs to the dictionary, d if priv == 'is_public': d['is_public'] = str('1') d['is_friend'] = str('0') d['is_family'] = str('0') elif priv == 'is_friend': d['is_public'] = str('0') d['is_friend'] = str('1') d['is_family'] = str('0') elif priv == 'is_family': d['is_public'] = str('0') d['is_friend'] = str('0') d['is_family'] = str('1') # Sign the api call sig = self.signCall( d ) d[ api.sig ] = sig d[ api.key ] = FLICKR[ api.key ] # Build request, open it, parse the return url = self.build_request(api.upload, d, (photo,)) xml = urllib2.urlopen( url ).read() res = xmltramp.parse(xml) # Rotate, if needed if rotation != 0: self.rotatePhoto( res.photoid, rotation, image ) # Add to a set, if needed if len(st) > 0: self.addToSet( st, res.photoid ) except: print str(sys.exc_info()) def addToSet( self, setList, photo_id ): """ Adds a photo to a already existing photoset """ if ( not self.checkToken() ): self.authenticate() # Loop thru each set in setList for photoset_id in setList: sd = {api.token : str(self.token), api.method : 'flickr.photosets.addPhoto', api.photoset_id : str(photoset_id), api.photo_id : str(photo_id)} # Sign the api call ssig = self.signCall( sd ) # Add the signature and api key to dictionary object sd[ api.sig ] = ssig sd[ api.key ] = FLICKR[ api.key ] # Encode the dictionary keys/values into a url string sparams = urllib.urlencode(sd) # Execute the POST call setUrl = urllib.urlopen(api.rest, sparams) # Try and get a response and some answers try: #response = self.getResponse( setUrl ) print 'Added to set:',photoset_id except: print 'Error adding to photoset:',str(sys.exc_info()) def logUpload( self, photoID, imageName ): photoID = str( photoID ) imageName = str( imageName ) self.uploaded[ imageName ] = photoID self.uploaded[ photoID ] = imageName # build_request/encode_multipart_formdata code is from www.voidspace.org.uk/atlantibots/pythonutils.html def build_request(self, theurl, fields, files, txheaders=None): """ Given the fields to set and the files to encode it returns a fully formed urllib2.Request object. You can optionally pass in additional headers to encode into the opject. (Content-type and Content-length will be overridden if they are set). fields is a sequence of (name, value) elements for regular form fields - or a dictionary. files is a sequence of (name, filename, value) elements for data to be uploaded as files. """ content_type, body = self.encode_multipart_formdata(fields, files) if not txheaders: txheaders = {} txheaders['Content-type'] = content_type txheaders['Content-length'] = str(len(body)) return urllib2.Request(theurl, body, txheaders) def encode_multipart_formdata(self,fields, files, BOUNDARY = '-----'+mimetools.choose_boundary()+'-----'): """ Encodes fields and files for uploading. fields is a sequence of (name, value) elements for regular form fields - or a dictionary. files is a sequence of (name, filename, value) elements for data to be uploaded as files. Return (content_type, body) ready for urllib2.Request instance You can optionally pass in a boundary string to use or we'll let mimetools provide one. """ CRLF = '\r\n' L = [] if isinstance(fields, dict): fields = fields.items() for (key, value) in fields: L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) for (key, filename, value) in files: filetype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' L.append('--' + BOUNDARY) L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename)) L.append('Content-Type: %s' % filetype) L.append('') L.append(value) L.append('--' + BOUNDARY + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % BOUNDARY return content_type, body def isGood( self, res ): if ( not res == "" and res('stat') == "ok" ): return True else : return False def reportError( self, res ): try: print "Error:", str( res.err('code') + " " + res.err('msg') ) except: print "Error: " + str( res ) def getResponse( self, url ): """ Send the url and get a response. Let errors float up """ xml = urllib2.urlopen( url ).read() return xmltramp.parse( xml ) def run( self ): while ( True ): self.upload() print "Last check: " , str( time.asctime(time.localtime())) time.sleep( SLEEP_TIME ) if __name__ == "__main__": flick = Uploadr() if ( len(sys.argv) >= 2 and sys.argv[1] == "-d"): flick.run() else: flick.upload()