#!/usr/bin/python # Original Author: # Michele Campeotto # (C) 2004 Michele Campeotto. # Yahoo! authorization code by: # Cameron Mallory # Glue and debugging: # Eric Weigle # # License: GNU GPL version 2. import gtk, gobject import sys, os, os.path, thread, time import urllib, urllib2, urlparse, mimetools, mimetypes import webbrowser import ConfigParser import xmltramp import shelve, md5 CONFIG_FILE = "~/.flickr_uploadr"; EDITOR_URL = "http://www.flickr.com/tools/uploader_edit.gne?ids=" FLICKR = {"title": "", "description": "", "tags": "", "is_public": "1", "is_friend": "0", "is_family": "0", "secret" : "fb2377b77bd2639c", "api_key" : "2d7076217eb2dc94997cba1bb61bd5b5", } # ====================================================================== 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" def __init__( self ): pass api = APIConstants() # ====================================================================== class FlickrYahooAuth: perms = "" # ---------------------------------------------------------------------- def __init__(self, uploadr): self.uploadr = uploadr self.config = ConfigParser.ConfigParser() self.config.read(os.path.expanduser(CONFIG_FILE)) if self.config.has_option('authorization', 'token'): self.token = self.config.get('authorization', 'token') else: self.token = None # ---------------------------------------------------------------------- # Authenticate user so we can upload images. def authenticate(self): # Already authenticated? (Only needs to occur ONCE). if self.checkToken(): return self.uploadr.progress.set_text("Authenticating to Flickr, step 1/4") self.getFrob() self.uploadr.progress.set_text("Authenticating to Flickr, step 2/4") self.getAuthKey() self.uploadr.progress.set_text("Authenticating to Flickr, step 3/4") self.getToken() self.uploadr.progress.set_text("Authenticating to Flickr, step 4/4") self.cacheToken() self.uploadr.progress.set_text("Authentication to Flickr complete.") # ---------------------------------------------------------------------- """ 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 # ---------------------------------------------------------------------- """ 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 ) print "Frob response:",response if ( self.isGood( response ) ): FLICKR[ api.frob ] = str(response.frob) else: self.reportError( response ) sys.exit(1) except: print "Error getting frob: ", str( sys.exc_info() ) sys.exit(1) # ---------------------------------------------------------------------- """ 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 ) dlg = gtk.MessageDialog(self.uploadr.window, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_QUESTION, gtk.BUTTONS_NONE, """This program can't upload photos to your account unless you tell Flickr that you trust it to do so. This is a two step process. First, you must log in to Flickr in a web browser and authorize this program. Then, the program must check with Flickr to verify it. Click "OK" now to authorize this program to upload photos. Click "Cancel" to quit the program. """) dlg.add_button(gtk.STOCK_OK, gtk.RESPONSE_OK) dlg.add_button(gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) dlg.set_resizable(False) result = dlg.run() dlg.destroy() if (result == gtk.RESPONSE_OK): try: webbrowser.open( url ) except: print str(sys.exc_info()) dlg = gtk.MessageDialog(self.uploadr.window, gtk.DIALOG_DESTROY_WITH_PARENT, gtk.MESSAGE_INFO, gtk.BUTTONS_OK, """Your web browser should be launching with instructions on authentication. If you have a browser open, the page may be in an existing window. Once you are done authorizing this application, return here and click OK.""") dlg.set_resizable(False) dlg.run() dlg.destroy() else: sys.exit(1) # ---------------------------------------------------------------------- """ 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 : 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 ) sys.exit(1) except: print str(sys.exc_info()) # ---------------------------------------------------------------------- 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 cacheToken( self ): try: if not 'authorization' in self.config.sections(): self.config.add_section('authorization') self.config.set('authorization', 'token', str(self.token)) os.umask(0177) self.config.write(open(os.path.expanduser(CONFIG_FILE), 'w')) except: print "Can't save authorization token! Error: ", str(sys.exc_info()) # ---------------------------------------------------------------------- '''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 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 ) # ====================================================================== class Uploadr: def __init__(self): self.authorization = FlickrYahooAuth(self) self.alpha = 25 self.window = gtk.Window(gtk.WINDOW_TOPLEVEL) self.window.set_title("Uploadr") self.window.connect("delete_event", self.delete_event) self.window.set_border_width(10) self.window.set_resizable(False) self.vbox = gtk.VBox(False) self.vbox.set_spacing(10) self.frame = gtk.Frame() self.frame.set_shadow_type(gtk.SHADOW_IN) self.frame.set_size_request(150, 150) self.frame.show() self.white = gtk.gdk.Pixbuf(gtk.gdk.COLORSPACE_RGB, False, 8, 150, 150) try: self.white.fill(0xffffffffL) except OverflowError: print "I'm sorry I can't get white to work on your Python." print "You'll have a black background instead." self.white.fill(0) # looks like crap, but at least doesn't crash self.image = gtk.Image() self.logo = self.white_background(gtk.gdk.pixbuf_new_from_file( os.path.join(sys.prefix, "share/FlickrUploadr/flickr_logo.gif"))) self.image.set_from_pixbuf(self.logo) self.image.show() self.frame.add(self.image) self.vbox.pack_start(self.frame, True, True) self.progress = gtk.ProgressBar() self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT) self.progress.set_text("Drop pictures here") self.vbox.pack_start(self.progress, False, False) self.progress.show() # to support drops from Konqueror I must do everything by hand, # default handlers don't work self.vbox.connect('drag_motion', self.motion_cb) self.vbox.connect('drag_drop', self.drop_cb) self.vbox.connect("drag_data_received", self.receive_callback) self.vbox.drag_dest_set(0, [], 0) self.window.add(self.vbox) self.vbox.show() self.window.show() self.authorization.authenticate() if len(sys.argv) > 2: self.upload_files(sys.argv[2:]) def motion_cb(self, widget, context, x, y, time): context.drag_status(gtk.gdk.ACTION_COPY, time) return True def drop_cb(self, widget, context, x, y, time): widget.drag_get_data(context, 'text/uri-list', time) return True def delete_event(self, widget, event, data=None): gtk.main_quit() return False # This is the main function to actually upload images after user drag/drops them # onto the main gtk window. def receive_callback(self, widget, context, x, y, selection, targetType, timestamp): self.upload_files( [urllib.unquote(urlparse.urlsplit(name)[2]) for name in selection.data.split('\r\n')[:-1] if os.path.isfile(urllib.unquote(urlparse.urlsplit(name)[2]))]) def upload_files(self, files): # FIXME: this will upload everything you pass to it, # should check for valid image files info = UploadDetails(self.window).run() if info is None: return else: self.open_flickr = info['open_flickr'] del(info['open_flickr']) FLICKR.update(info) self.photoids = [] for filename in files: current = files.index(filename) self.progress.set_text("%s" % (filename.split("/")[-1],)) self.progress.set_fraction(float(current) / len(files)) pixbuf = gtk.gdk.pixbuf_new_from_file(filename) # resize to fit in frame if pixbuf.get_height() < pixbuf.get_width(): width = 120 height = int(120.0 / pixbuf.get_width() * pixbuf.get_height()) else: height = 120 width = int(120.0 / pixbuf.get_height() * pixbuf.get_width()) pixbuf = pixbuf.scale_simple(width, height, gtk.gdk.INTERP_TILES) pixbuf = self.white_background(pixbuf) # fade gobject.timeout_add(50, self.do_fade, self.image.get_pixbuf().copy(), pixbuf) finish = thread.allocate_lock() finish.acquire() thread.start_new_thread(self.upload, (filename, finish)) while finish.locked(): while gtk.events_pending(): gtk.main_iteration(False) time.sleep(0.05) if self.open_flickr: webbrowser.open(EDITOR_URL+','.join(self.photoids)) gobject.timeout_add(50, self.do_fade, self.image.get_pixbuf().copy(), self.logo) self.progress.set_text("Drop pictures here") self.progress.set_fraction(0.0) def white_background(self, image): x = (150 - image.get_width()) / 2 y = (150 - image.get_height()) / 2 composed = self.white.copy() image.composite(composed, x, y, min(150, image.get_width()), min(150, image.get_height()), x, y, 1, 1, gtk.gdk.INTERP_TILES, 255) return composed def do_fade(self, pixbuf, new_pixbuf): new_pixbuf.composite(pixbuf, 0, 0, pixbuf.get_width(), pixbuf.get_height(), 0, 0, 1, 1, gtk.gdk.INTERP_TILES, self.alpha) self.image.set_from_pixbuf(pixbuf) self.alpha += 25 if self.alpha < 255: return True else: self.alpha = 25 self.image.set_from_pixbuf(new_pixbuf) return False # ---------------------------------------------------------------------- def upload(self, filepath, finish): filename = filepath.split("/")[-1] try: photo = ('photo', filename, open(filepath,'rb').read()) except IOError: self.progress.set_text("Failed to read file data from %s" % filename) finish.release() thread.exit() d = { api.token : str(self.authorization.token), api.perms : str(self.authorization.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.authorization.signCall( d ) d[ api.sig ] = sig d[ api.key ] = FLICKR[ api.key ] url = build_request(api.upload, d, (photo,)) xml = urllib2.urlopen( url ).read() response = xmltramp.parse(xml) if (not self.authorization.isGood( response )): print "Upload of file %s failed. Quit application now!\n" % filename self.progress.set_text("Upload of file %s failed. Quit application now!\n" % filename) self.authorization.reportError(response) # We do not unlock the condition variable. # This is a purposeful deadlock so user can't get even more hosed. else: self.photoids.append(str(response.photoid)) finish.release() thread.exit() class UploadDetails(gtk.Dialog): def __init__(self, parent): self.config = ConfigParser.ConfigParser() self.config.read(os.path.expanduser(CONFIG_FILE)) gtk.Dialog.__init__(self, "Pictures information", parent, gtk.DIALOG_MODAL, (gtk.STOCK_OK, gtk.RESPONSE_ACCEPT, gtk.STOCK_CANCEL, gtk.RESPONSE_REJECT)) table = gtk.Table(4, 2) table.set_border_width(10) table.set_row_spacings(10) label = gtk.Label("Title: ") label.set_alignment(1, 0.5) table.attach(label, 0, 1, 0, 1) self.ptitle = gtk.Entry() table.attach(self.ptitle, 1, 2, 0, 1) label = gtk.Label("Tags: ") label.set_alignment(1, 0.5) table.attach(label, 0, 1, 1, 2) self.tags = gtk.Entry() table.attach(self.tags, 1, 2, 1, 2) self.is_private = gtk.CheckButton("The pictures are private") if self.config.has_option('settings', 'is_public'): self.is_private.set_active(not self.config.getboolean('settings', 'is_public')) table.attach(self.is_private, 1, 2, 2, 3) self.open_flickr = gtk.CheckButton("Open Flickr after upload") if self.config.has_option('settings', 'open_flickr'): self.open_flickr.set_active(self.config.getboolean('settings', 'open_flickr')) else: self.open_flickr.set_active(True) table.attach(self.open_flickr, 1, 2, 3, 4) self.vbox.pack_start(table, True, True) self.show_all() def run(self): response = gtk.Dialog.run(self) info = {"title": self.ptitle.get_text(), "tags": self.tags.get_text(), "is_public": self.is_private.get_active() and "0" or "1", "open_flickr": self.open_flickr.get_active() and True or False } self.destroy() if response == gtk.RESPONSE_ACCEPT: self.config = ConfigParser.ConfigParser() # this succeeds by returning [] even if config is nonexistent. self.config.read(os.path.expanduser(CONFIG_FILE)) if not 'settings' in self.config.sections(): self.config.add_section('settings') self.config.set('settings', 'is_public', info["is_public"]) self.config.set('settings', 'open_flickr', info["open_flickr"]) os.umask(0177) self.config.write(open(os.path.expanduser(CONFIG_FILE), 'w')) return info else: return None # This code is from www.voidspace.org.uk/atlantibots/pythonutils.html def encode_multipart_formdata(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)) # if not filetype.startswith('text'): # L.append('Content-Transfer-Encoding: binary') L.append('Content-Type: %s' % filetype) # L.append('Content-Length: %s\r\n' % str(len(value))) 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 build_request(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 = 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) if __name__ == "__main__": hello = Uploadr() gtk.main()