#!/usr/bin/env python

import sys, time, os, urllib2, shelve, string, xmltramp, mimetools, mimetypes, md5, webbrowser, urllib
#
#   uploadrExt.py
#
#   Upload images placed within a directory to your Flickr account.
#   You can create specific set/tags for the directory photos by assigning arguments
#
#   Requires:
#       xmltramp http://www.aaronsw.com/2002/xmltramp/
#       flickr account http://flickr.com
#
#   Original script uploadr.py from:
#	http://berserk.org/uploadr/
#
#   Inspired by:
#       http://micampe.it/things/flickruploadr
#
#   Extended by Raphael Schiller:
#	http://www.schiller.cc/blog/programming/uploadr2setpy/
#
#   Extended by Daniel Kao
#	http://www.linux.org.tw/~plateau/uploadrextpy/

#   Usage:
#
#   % ./uploadrExt.py directory_name set_name tag1 tag2 tag3 ...
#
#   October 2007
#   Daniel Kao
#
#   This code has been updated to use the new Auth API from flickr.
#
#   You may use this code however you see fit in any form whatsoever.
#
##
##  Items you will want to change
## 

#
#   Flickr settings
#
FLICKR = {"title": "",
        "description": "",
        "tags": "",
		"photoset" : "",
        "is_public": "1",
        "is_friend": "0",
        "is_family": "0"}
#
#   File we keep the history of uploaded images in.
#
HISTORY_FILE = "uploadr.history"

##
##  You shouldn't need to modify anything below here
##
FLICKR["secret" ] = "fb2377b77bd2639c"
FLICKR["api_key" ] = "2d7076217eb2dc94997cba1bb61bd5b5"
class APIConstants:
    base = "http://api.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"
    title = "title"
    primary_photo_id = "primary_photo_id"
    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()



    """
    Signs args via md5 per http://www.flickr.com/services/api/auth.spec.html (Section 8)
    """
    def signCall( self, data):
        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
        
 
    #
    #   Authenticate user so we can upload images
    #
    def authenticate( self ):
        print "Getting new Token"
        self.getFrob()
        self.getAuthKey()
        self.getToken()   
        self.cacheToken()

    """
    flickr.auth.getFrob
    
    Returns a frob to be used during authentication. This method call must be 
    signed.
    
    This method does not require authentication.
    Arguments
    
    api.key (Required)
    Your API application key. See here for more details.     
    """
    def getFrob( self ):
        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() )

    """
    Checks to see if the user has authenticated this application
    """
    def getAuthKey( self ): 
        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()    

    """
    http://www.flickr.com/services/api/flickr.auth.getToken.html
    
    flickr.auth.getToken
    
    Returns the auth token for the given frob, if one has been attached. This method call must be signed.
    Authentication
    
    This method does not require authentication.
    Arguments
    
    NTC: We need to store the token in a file so we can get it and then check it insted of
    getting a new on all the time.
        
    api.key (Required)
       Your API application key. See here for more details.
    frob (Required)
       The frob to check.         
    """   
    def getToken( self ):
        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() )

    """
    Attempts to get the flickr token from disk.
    """
    def getCachedToken( self ): 
        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())

    """
    flickr.auth.checkToken

    Returns the credentials attached to an authentication token.
    Authentication
    
    This method does not require authentication.
    Arguments
    
    api.key (Required)
        Your API application key. See here for more details.
    auth_token (Required)
        The authentication token to check. 
    """
    def checkToken( self ):    
        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 upload( self , setName ):
        newImages = self.grabNewImages()
        i = 0
        if ( not self.checkToken() ):
            self.authenticate()
        self.uploaded = shelve.open( HISTORY_FILE )
        for image in newImages:
            self.uploadImage( image , setName)
        
    def grabNewImages( self ):
        images = []
        sets = []
        foo = os.walk( IMAGE_DIR )
        for data in foo:
            (dirpath, dirnames, filenames) = data
            for f in filenames :
                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 uploadImage( self, image, set ):
        if ( not self.uploaded.has_key( image ) ):
            print "Uploading ", image , "...",
            try:
                photo = ('photo', image, open(image,'rb').read())
                d = {
                    api.token   : str(self.token),
                    api.perms   : str(self.perms),
                    "tags"      : str( FLICKR["tags"] ),
                    "is_public" : str( FLICKR["is_public"] ),
                    "is_friend" : str( FLICKR["is_friend"] ),
                    "is_family" : str( FLICKR["is_family"] )
                }
                sig = self.signCall( d )
                d[ api.sig ] = sig
                d[ api.key ] = FLICKR[ api.key ]        
                url = self.build_request(api.upload, d, (photo,))    
                xml = urllib2.urlopen( url ).read()
                res = xmltramp.parse(xml)
                if ( self.isGood( res ) ):
                    print "successful."
                    self.logUpload( res.photoid, image )
                    # insert into set when set is not empty
                    if set != "":
                        # check if the set is created already
                        if FLICKR["photoset"] != "":
                            self.addPhoto(FLICKR["photoset"], res.photoid)
                        # check if the set name already exists on server
                        else:
                            setID = self.getSetID(set)
                            if setID == None:
                                self.createSet( set, res.photoid)
                            else:
                                FLICKR["photoset"] = setID
                                self.addPhoto(FLICKR["photoset"], res.photoid)
                else :
                    print "problem.."
                    self.reportError( res )
            except:
                print "Error uploading: ", 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        # XXX what if no files are encoded
        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 )

    """
    Send the url and get a response.  Let errors float up
    """
    def getResponse( self, url ):
        xml = urllib2.urlopen( url ).read()
        return xmltramp.parse( xml )
	
    def createSet( self, title, photo_id ):
        if ( not self.checkToken() ):
            self.authenticate()
        d = { 
            api.token   : str(self.token),
            api.method  : "flickr.photosets.create",
            api.title : str(title),
            api.primary_photo_id : str(photo_id)
            }
        sig = self.signCall( d )
        d[ api.title ] =  urllib.quote_plus( title ) 
        url = self.urlGen( api.rest, d, sig )
        try:
            response = self.getResponse( url )
            if ( self.isGood( response ) ):
                FLICKR["photoset"] = str( response.photoset('id') )
                print "New photoset: ", response.photoset["title"]
            else:
                self.reportError( response )
        except:
            print "Error creating photoset:" , str( sys.exc_info() )

    def addPhoto( self, photoset_id, photo_id ):
        if ( not self.checkToken() ):
            self.authenticate()
        d = { 
            api.token   : str(self.token),
            api.method  : "flickr.photosets.addPhoto",
            api.photoset_id : str(photoset_id),
            api.photo_id : str(photo_id)
            }
        sig = self.signCall( d )
        url = self.urlGen( api.rest, d, sig )
        try:
            response = self.getResponse( url )
            print "Added to set: ", photoset_id
        except:
            print "Error adding photo to photoset:" , str( sys.exc_info() )

    def getList( self ):
        if ( not self.checkToken() ):
            self.authenticate()
        d = { 
            api.token   : str(self.token),
            api.method  : "flickr.photosets.getList",
            }
        sig = self.signCall( d )
        url = self.urlGen( api.rest, d, sig )
        try:
            response = self.getResponse( url )
            if ( self.isGood( response ) ):
                print "get set list succeeds"
                return response.photosets
            else:
                self.reportError( response )
        except:
            print "Error getting set list" , str( sys.exc_info() )

    # if set exists already, the photoset id will be retrieved.
    def getSetID( self , set):
        setList = self.getList()
        for photoset in setList:
            if str(photoset["title"]) == set:
                return photoset("id")
            
if __name__ == "__main__":
    if ( len(sys.argv) == 1):
        print """1. to insert photos into a set
    % ./uploadr.py directory_name set_name tag1 tag2 tag3 ...

    2. to insert photos into main photo stream
    % ./uploadr.py directory_name -d tag1 tag2 tag3 ..."""
        exit()

    flick = Uploadr()
    IMAGE_DIR = sys.argv[1]

    if ( len(sys.argv) == 2):
        flick.upload("")
        exit()

    # add tags
    if ( len(sys.argv) >=4):
        tags = ",".join(sys.argv[3:])
        FLICKR["tags"]=tags

    # upload into set
    if sys.argv[2] != "-d":
        flick.upload(sys.argv[2])
    # upload into main stream
    else:
        flick.upload("")

