"""A Python library for accessing FotoBilder, the photo hosting software that runs pics.livejournal.com Stuart Langridge, http://www.kryogenix.org/ v 0.1, 24/04/2007 """ import os, urlparse from xml.dom import minidom from xml.xpath import Evaluate from md5 import md5 from ProgressReportingHTTPConnection import ProgressReportingHTTPConnection ENDPOINT = "http://pics.livejournal.com/interface/simple" class FotoBilder: def __init__(self, user, password, progress_callback=None): self.user = user self.password = password self.progress_callback = progress_callback self.CURRENT_CHALLENGE = "" scheme,self.server,self.url,urlparams,query,fragment = urlparse.urlparse(ENDPOINT) self.GetChallenge() # prime the system with a challenge def CreateGals(self, galleries): raise NotImplementedError def GetChallenge(self): """Get a challenge to be used in later authentication. You should not need to call this. This library takes care of auth challenges for you. """ dom = self.__send("GET",{"Mode":"GetChallenge"}) return Evaluate( "/FBResponse/GetChallengeResponse/Challenge/text()",dom)[0].nodeValue def GetChallenges(self, quantity): raise NotImplementedError def GetGals(self): dom = self.__send("GET",{"Mode":"GetGals"}) gals = [] for galnode in Evaluate("/FBResponse/GetGalsResponse/Gal",dom): gal = {} gal.update(dict(galnode.attributes.items())) for node in galnode.childNodes: if node.nodeType == dom.TEXT_NODE: continue if node.nodeName == "GalMembers": gal["GalMembers"] = [x.getAttribute("id") for x in Evaluate("GalMember",node)] elif node.nodeName == "ParentGals": gal["ParentGals"] = [x.getAttribute("id") for x in Evaluate("ParentGal",node)] elif node.nodeName == "ChildGals": gal["ChildGals"] = [x.getAttribute("id") for x in Evaluate("ChildGal",node)] else: try: gal[node.nodeName] = node.firstChild.nodeValue except: gal[node.nodeName] = '' gals.append(gal) return gals def GetGalsTree(self): raise NotImplementedError def GetPics(self): dom = self.__send("GET",{"Mode":"GetPics"}) pics = [] for picnode in Evaluate("/FBResponse/GetPicsResponse/Pic",dom): pic = {} pic.update(dict(picnode.attributes.items())) for node in picnode.childNodes: if node.nodeType == dom.TEXT_NODE: continue if node.nodeName == "Meta": try: pic[node.getAttribute("name")] = node.firstChild.nodeValue except: pass else: try: pic[node.nodeName] = node.firstChild.nodeValue except: pic[node.nodeName] = '' pics.append(pic) return pics def GetSecGroups(self): raise NotImplementedError def Login(self): raise NotImplementedError def UploadPic(self, image_filename, galleries=[], filename_is_receipt=False, filename_is_data=False): """Upload a picture Arguments: image_filename - a path to an image galleries - a list of gallery namelists filename_is_receipt - if you have a receipt for this image, returned from UploadPrepare, then set image_filename to the receipt and filename_is_receipt to True. filename_is_data - if you want to pass imagedata rather than a filename, set image_filename to the data and filename_is_data to True. A gallery namelist is a list of path components for a gallery. So a gallery "level3" inside a gallery "level2" inside a gallery "level1" would be passed as ["level1","level2","level3"]. You pass a list of these lists. Interim galleries will be created. So, if there are currently no galleries at all, and you pass the following as galleries: [["level1","level2","gal1"],["level1","level2a","gal2"],["level1a"]] then at the end there will be galleries /level1/level2/gal1, /level1/level2a/gal2, and /level1a. """ param_data = { "Mode":"UploadPic" } if galleries: param_data["UploadPic.Gallery._size"] = len(galleries) count = 0 for g in galleries: gname = g[-1] if len(g) > 1: pathelements = g[:-1] pathcount = 0 param_data["UploadPic.Gallery.%s.Path._size" % count] = len(pathelements) for pathel in pathelements: param_data["UploadPic.Gallery.%s.Path.%s" % (count,pathcount)] = pathel pathcount += 1 param_data["UploadPic.Gallery.%s.GalName" % count] = gname count += 1 if filename_is_receipt: param_data["UploadPic.Receipt"] = image_filename return self.__send("PUT",param_data) elif filename_is_data: return self.__send("PUT",param_data,image_filename) else: fp = open(image_filename,"rb") image_data = fp.read() fp.close() return self.__send("PUT",param_data,image_data) def UploadPrepare(self, image_filenames): """Checks to see if images have been uploaded before. Returns a dict keyed on image filename. Each value is a dict with key "status". If an image has been uploaded already, status == "existing" and the value dict also contains a key "receipt" with a value that can be passed to UploadPic. If the image has not already been uploaded then status == "new". Actually calls __UploadPrepare to do the work, because you can't UploadPrepare more than about 5 images at a time. """ pics = {} for i in range(0,len(image_filenames),5): pics.update(self.__UploadPrepare(image_filenames[i:i+5])) return pics def __UploadPrepare(self,image_filenames): param_data = {} param_data["UploadPrepare.Pic._size"] = len(image_filenames) count = 0 prepared = {} for f in image_filenames: picmagic, picmd5, picsize = self.CalculateImageAttributes(f) prepared[picmd5] = f param_data["UploadPrepare.Pic.%s.MD5" % count] = picmd5 param_data["UploadPrepare.Pic.%s.Magic" % count] = picmagic param_data["UploadPrepare.Pic.%s.Size" % count] = picsize count += 1 return self.__UploadPrepareSend(param_data,prepared) def CalculateImageAttributes(self, image): fp = open(image,"rb") data = fp.read() fp.close() picmagic = ''.join([('0'+hex(ord(x))[2:])[-2:] for x in data[:10]]) picmd5 = md5(data).hexdigest() size = len(data) return (picmagic, picmd5, size) def UploadPrepareWithData(self, image_dict): """Checks to see if images have been uploaded before. Used when you already have magic and md5 and size data, rather than wanting them calculated for you. image_dict - a dictionary of images: { "path to image" : { "md5" : md5 hex digest of image data, "magic": magic key for image data, "size": size of image data }, ... } """ pics = {} items = image_dict.items() for i in range(0,len(items),5): pics.update(self.__UploadPrepareWithData(dict(items[i:i+5]))) print "Sending",i return pics def __UploadPrepareWithData(self, image_dict): param_data = {} param_data["UploadPrepare.Pic._size"] = len(image_dict.keys()) count = 0 prepared = {} for filename, data in image_dict.items(): prepared[data["md5"]] = filename param_data["UploadPrepare.Pic.%s.MD5" % count] = data["md5"] param_data["UploadPrepare.Pic.%s.Magic" % count] = data["magic"] param_data["UploadPrepare.Pic.%s.Size" % count] = data["size"] count += 1 return self.__UploadPrepareSend(param_data,prepared) def __UploadPrepareSend(self, param_data, prepared): """Actually send the UploadPrepare data to the server""" dom = self.__send("GET",param_data) pics = {} for picnode in Evaluate('/FBResponse/UploadPrepareResponse/Pic',dom): picdata = {} picmd5 = Evaluate("MD5",picnode)[0].firstChild.nodeValue known = picnode.getAttribute("known") if known == '': # some kind of error with this picture picdata["status"] = "error" errornodes = Evaluate("Error",picnode) if errornodes: picdata["errormessage"] = errornodes[0].firstChild.nodeValue picdata["errorcode"] = errornodes[0].getAttribute("code") elif known == '0': picdata["status"] = "new" else: picdata["status"] = "exists" picdata["receipt"] = Evaluate("Receipt", picnode)[0].firstChild.nodeValue picfilename = prepared[picmd5] pics[picfilename] = picdata return pics def UploadTempFile(self, image_filename): raise NotImplementedError def AbortCurrentConnection(self): if self.con: self.con.abort_connection() def __send(self,method, params, body="", returnxml=True): self.con = ProgressReportingHTTPConnection(self.server, progress_callback=self.progress_callback) headers = self.__params_to_headers(params) # explicitly add required headers headers["X-FB-User"] = self.user headers["X-FB-GetChallenge"] = "1" # if we have a current challenge, use it to add an Auth header if self.CURRENT_CHALLENGE: auth = md5(self.CURRENT_CHALLENGE + md5(self.password).hexdigest()).hexdigest() auth = "crp:%s:%s" % (self.CURRENT_CHALLENGE,auth) headers["X-FB-Auth"] = auth self.con.request(method,self.url,body,headers) try: response = self.con.getresponse() except: raise data = response.read() # Extract the challenge from the response so we've got it for next time dom = minidom.parseString(data) try: self.CURRENT_CHALLENGE = Evaluate( "/FBResponse/GetChallengeResponse/Challenge/text()",dom)[0].nodeValue except: print "FAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAIL" print headers print data print "FAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAILFAIL" raise "Connection to LJ failed" if returnxml: return dom else: return data def __params_to_headers(self,params): ret = {} for k,v in params.items(): ret["X-FB-"+k] = v return ret if __name__ == "__main__": fb = FotoBilder("stuartlangridge","") print fb.GetGals() #print fb.GetPics() #out = fb.UploadPrepare(["/home/aquarius/Default.png"]) #receipt = out['/home/aquarius/Default.png']['receipt'] fb.UploadPic("/home/aquarius/Photos/2002/9/25/LinuxMan.jpg", [["new"]])