Shutterfly Patch



Here is a long overdue patch to the Shutterfly module.  I was spending
too much time trying to decode javascript and not enough time paying
attention to the cookies in wireshark.

Fixes (that I can recall):
Simultaneous logins now work
The correct LUID is returned after uploading a photo
Uploading no longer hangs, worked around a bug in urllib2

Additions:
'delete' support
'get' support (but Shutterfly is only a data sink for now)

That's the important stuff.  Let me know if there are any issues.

Jeremy
Index: conduit/modules/ShutterflyModule/ShutterflyModule.py
===================================================================
--- conduit/modules/ShutterflyModule/ShutterflyModule.py	(revision 1220)
+++ conduit/modules/ShutterflyModule/ShutterflyModule.py	(working copy)
@@ -9,6 +9,7 @@
 from conduit.datatypes import Rid
 import conduit.dataproviders.Image as Image
 import conduit.Exceptions as Exceptions
+import conduit.datatypes.Photo as Photo
 
 Utils.dataprovider_add_dir_to_path(__file__, "shutterfly")
 from shutterfly import Shutterfly
@@ -16,9 +17,10 @@
 from gettext import gettext as _
 
 MODULES = {
-	"ShutterflySink" : {"type" : "dataprovider"}
+	"ShutterflySink" : {"type" : "dataprovider"},
 }
 
+
 class ShutterflySink(Image.ImageSink):
 	
 	_name_ = _("Shutterfly")
@@ -37,7 +39,10 @@
 		self.sapi = None
 		self.salbum = None
 		self.sphotos = None
-	
+
+	def _get_raw_photo_url(self, photoInfo):
+		return photoInfo.url
+
 	def _get_photo_info(self, id):
 		if self.sphotos.has_key(id):
 			return self.sphotos[id]
@@ -62,18 +67,33 @@
 	def get_all(self):
 		return self.sphotos.keys()
 	
+	def get(self, LUID):
+		Image.ImageSink.get(self, LUID)
+		sphoto = self.sphotos[LUID]
+
+		f = ShutterflyPhoto(URI=sphoto.url)
+		f.set_open_URI(sphoto.url)
+		f.set_UID(LUID)
+
+		return f
+
 	def delete(self, LUID):
 		"""
 		Delete a photo by ID
+		Deleting a photo invalidates album length and photo index values.
+		We must reload the photos (or do something else...)
 		"""
 		if not self.sphotos.has_key(LUID):
 			log.warn("Photo does not exist")
 			return
+
+		try:
+			self.salbum.deletePhoto(self.sphotos[LUID])
+		except Exception, e:
+			raise Exceptions.SyncronizeError("Shutterfly Delete Error - Try Again.")
+
+		self.sphotos = self.salbum.getPhotos()
 		
-		# Need to figure out how to delete a photo (javascript hell)
-		#self.salbum.deletePhoto(self.sphotos[LUID])
-		del self.sphotos[LUID]
-	
 	def _upload_photo(self, uploadInfo):
 		"""
 		Upload to album
@@ -136,5 +156,5 @@
 		return True
 	
 	def get_UID(self):
-		return self.username
+		return self.username+":"+self.album
 
Index: conduit/modules/ShutterflyModule/shutterfly/shutterfly.py
===================================================================
--- conduit/modules/ShutterflyModule/shutterfly/shutterfly.py	(revision 1220)
+++ conduit/modules/ShutterflyModule/shutterfly/shutterfly.py	(working copy)
@@ -31,6 +31,7 @@
 from gettext import gettext as _
 
 FORMAT_STRING = _("%Y-%m-%d %H:%M:%S")
+PERPAGE = 80									# Shutterfly shows 80 pics per small page
 
 ###############################################################################
 # Helper functions
@@ -49,6 +50,17 @@
 		data = urllib.urlencode(data)
 	return urllib2.Request(url, data, headers)
 
+def sflyCookie(name, data):
+	if type(data) == dict:
+		value = ''
+		for item in data:
+			value += item + ':' + str(data[item]) + '&'
+		value = value[:-1]
+	else:
+		value = data
+	
+	return  cookielib.Cookie(version=0, name=name, value=value, port=None, port_specified=False, domain='.shutterfly.com', domain_specified=True, domain_initial_dot=True, path='/', path_specified=True, secure=False, expires=None, discard=True, comment=None, comment_url=None, rest={}, rfc2109=False)
+
 ###############################################################################
 # Shutterfly exception processor
 ###############################################################################
@@ -106,6 +118,7 @@
 	albumurl = "http://www.shutterfly.com/action/lightbox/server?action=aCount,aPage&pageNumber=%(page)s&activeAlbumIdx=0&ft=1&mode=albums&view=albums&singleSelect=false&ts=0&sscf=1"
 	addalbumurl = "http://www.shutterfly.com/view/album_create.jsp";
 	photourl = "http://www.shutterfly.com/action/lightbox/server?action=pFrame,pView,pCount,pPage&albumId=%(albumid)s&pageNumber=%(page)s&pictureSrc=A&ft=1&mode=pictures&view=small&singleSelect=false&ts=0&sscf=1"
+	delphotourl = "http://www.shutterfly.com/action/lightbox/server?action=deletePictures,pCount,pPage&albumId=%(albumid)s&pageNumber=%(page)s&pictureSrc=A&ft=1&mode=pictures&view=small&singleSelect=false&ts=0&sscf=1"
 	uploadurl = "http://www.shutterfly.com/add/upload_browse.jsp";
 	uploadimageurl = "http://up1.shutterfly.com/UploadImage";
 
@@ -126,6 +139,9 @@
 	
 	def _getphotourlbyid(aid, page):
 		return SFApi.photourl % {"albumid" : aid, "page" : page}
+
+	def _getdelphotourlbyid(aid, page):
+		return SFApi.delphotourl % {"albumid" : aid, "page" : page}
 	
 	def _getuploadurl():
 		return SFApi.uploadurl
@@ -139,10 +155,29 @@
 	getalbumurlbypage = staticmethod(_getalbumurlbypage)
 	getaddalbumurl = staticmethod(_getaddalbumurl)
 	getphotourlbyid = staticmethod(_getphotourlbyid)
+	getdelphotourlbyid = staticmethod(_getdelphotourlbyid)
 	getuploadurl = staticmethod(_getuploadurl)
 	getuploadimageurl = staticmethod(_getuploadimageurl)
 
 ###############################################################################
+# Shutterfly Response Handler
+###############################################################################
+class ShutterflyCookieProcessor(urllib2.HTTPCookieProcessor, urllib2.HTTPRedirectHandler):
+	"""
+	Kill form header stuff on a 302 redirect, getting rid of just content-length 
+	does not appear to help. Maybe just need to delete content-type but for now
+	I delete the whole header, meh.
+	Bug: 1401 - http://bugs.python.org/issue1401 
+	"""
+	def http_error_302(self, req, fp, code, msg, headers):
+		for key in req.headers.keys():
+			if key.lower() == 'content-length':
+				req.headers = {}
+		result = urllib2.HTTPRedirectHandler.http_error_301(
+			self, req, fp, code, msg, headers)
+		return result
+
+###############################################################################
 # Shutterfly connection object class
 ###############################################################################
 class ShutterflyConnection(object):
@@ -163,7 +198,7 @@
 		Login using credentials and aquired FID
 		"""
 		self.__cj = cookielib.CookieJar()
-		self.__opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(self.__cj))
+		self.__opener = urllib2.build_opener(ShutterflyCookieProcessor(self.__cj))
 		urllib2.install_opener(self.opener)
 
 		user = utf8(user)
@@ -173,13 +208,10 @@
 		request = mkRequest(SFApi.getaccounturl())
 		response = self.opener.open(request)
 		buf = response.read()
+		response.close()
 		
 		if buf.find(user) > 0:
 			# Found the specified user logged in.
-			# Need to add logging out functionality if wrong user logged in?
-			# Maybe conduit to conduit 'sessions' need to be unique?
-			# If another user is logged in, this will crash right now as opposed to
-			# placing specified photos in the other users acct.
 			return
 		
 		request = mkRequest(SFApi.getentryurl())
@@ -195,12 +227,16 @@
 		request = mkRequest(SFApi.getloginurl(), data, headers)
 		response = self.opener.open(request)
 		buf = response.read()
+		response.close()
 		
 		if buf.find("return.sfly") == -1:
 			raise ShutterflyException("Unable to connect (wrong credentials?)")
 	
 	def getfid(self):
 		return self.fid
+	
+	def setCookie(self, cookie):
+		self.__cj.set_cookie(cookie)
 
 ###############################################################################
 # Shutterfly api
@@ -217,6 +253,7 @@
 		request = mkRequest(SFApi.getalbumurlbypage(1))
 		response = self.__sc.opener.open(request)
 		buf = response.read()
+		response.close()
 		
 		if buf.find("var status='failure'") > -1:
 			raise ShutterflyException("Find albums page not retrieved (url changed?)")
@@ -230,9 +267,9 @@
 		l = {}
 
 		while count > 0:
-			pairs = re.findall("aList\[\d+\]='(.*)';\ntList\[\d+\]='(.*)';", buf)
-			for pair in pairs:
-				alb = ShutterflyAlbum(self.__sc, pair[0], pair[1])
+			details = re.findall("aList\[(\d+)\]='(.*)';\ntList\[\d+\]='(.*)';\ncList\[\d+\]='(.*)';", buf)
+			for items in details:
+				alb = ShutterflyAlbum(self.__sc, int(items[0]), items[1], items[2], int(items[3]))
 				l[alb.name] = alb
 			count -= perpage
 			page += 1
@@ -240,6 +277,7 @@
 				request = mkRequest(SFApi.getalbumurlbypage(page))
 				response = self.__sc.opener.open(request)
 				buf = response.read()
+				response.close()
 		
 		return l
 
@@ -258,6 +296,7 @@
 		request = mkRequest(SFApi.getaddalbumurl(), data, headers)
 		response = self.__sc.opener.open(request)
 		buf = response.read()
+		response.close()
 		
 		albums = self.getAlbums()
 		return albums[name]
@@ -266,18 +305,24 @@
 # Shutterfly album object
 ###############################################################################
 class ShutterflyAlbum(object):
+	__sc = None
+	__index = None
+	__id = None
 	__name = None
+	__length = None
+	
+	index = property(lambda s: s.__index)
+	id = property(lambda s: s.__id)
 	name = property(lambda s: s.__name)
-	id = property(lambda s: s.__id)
+	length = property(lambda s:  s.__length)
 	
-	__id = None
-	__sc = None
-	
-	def __init__(self, sc, id, name):
+	def __init__(self, sc, index, id, name, length):
 		""" Should only be called by Shutterfly """
 		self.__sc = sc
+		self.__index = index
+		self.__id = utf8(id)
 		self.__name = name
-		self.__id = utf8(id)
+		self.__length = length
 	
 	def getPhotos(self):
 		"""
@@ -286,6 +331,7 @@
 		request = mkRequest(SFApi.getphotourlbyid(self.__id, 1))
 		response = self.__sc.opener.open(request)
 		buf = response.read()
+		response.close()
 		
 		if buf.find("var status='failure'") > -1:
 			raise ShutterflyException("List photos page not retrieved (url changed?)")
@@ -299,9 +345,9 @@
 		l = {}
 		
 		while count > 0:
-			pairs = re.findall("pList\[\d+\]='(.*)';\ntList\[\d+\]='(.*)';", buf)
-			for pair in pairs:
-				photo = ShutterflyPhoto(pair[0], pair[1])
+			details = re.findall("pList\[(\d+)\]='(.*)';\ntList\[\d+\]='(.*)';", buf)
+			for items in details:
+				photo = ShutterflyPhoto(int(items[0]), page-1, items[1], items[2])
 				l[photo.id] = photo
 			count -= perpage
 			page += 1
@@ -309,10 +355,11 @@
 				request = mkRequest(SFApi.getphotourlbyid(self.__id, page))
 				response = self.__sc.opener.open(request)
 				buf = response.read()
+				response.close()
 		
 		return l
 	
-	def uploadPhoto(self, filename, mimeType, description=""):
+	def uploadPhoto(self, filename, mimeType, name, description = ""):
 		"""
 		Upload a photo to this album
 		"""
@@ -322,53 +369,88 @@
 			request = mkRequest(SFApi.getuploadurl())
 			response = self.__sc.opener.open(request)
 			buf = response.read()
+			response.close()
 			
 			data = []
-			for name in ['ProtocolVersion', 'RequestType', 'AuthenticationID', 
+			for item in ['ProtocolVersion', 'RequestType', 'AuthenticationID', 
 			             'PartnerID', 'PartnerSubID', 'previewURL', 'redirect', 
 			             'doNotDisplayFormAfterUpload']:
-				data.append((name, re.search('name="'+name+'" value="(.*)"', buf).group(1)))
+				data.append((item, re.search('name="'+item+'" value="(.*)"', buf).group(1)))
 			
 			data.append(('Image.AlbumID', self.__id))
 			data.append(('Image.AlbumName', self.__name))
 			data.append(('Image.UploadTime', time.strftime(FORMAT_STRING)))
 			
 			content_type, body = encode_multipart_formdata(data, 
-				[(filename, mimeType, open(filename, "rb").read())])
+				[(name, mimeType, open(filename, "rb").read())])
 			
 			headers = {"Content-Type" : content_type, 
 			           "Content-Length" : str(len(body)), }
 			
-			print headers
-			
 			request = urllib2.Request(SFApi.getuploadimageurl(), 
 			                          body, headers)
 			response = self.__sc.opener.open(request)
 			buf = response.read()
+			response.close()
 			
-			if buf.find("Pictures added successfully") == -1:
+			if response.geturl().find("Success=1") == -1:
 				raise ShutterflyException("Could not add photo")
 			
 			photoid = re.search('name="vcidList" value="(.*)"', buf).group(1)
-			return ShutterflyPhoto(photoid, os.path.basename(filename))
+			# Shutterfly started lying about the photo id.  Need to watch this value
+			# Maybe just find the new photo by running another get_all ?
+			photoid = photoid[:34] + '2' + photoid[35:]
+			self.__length += 1
+			return ShutterflyPhoto(self.length, 0, photoid, name)
 			
 		else:
 			raise ShutterflyException("File does not exist")
 	
 	def deletePhoto(self, photo):
-		pass
-	
+		cdata = {'mode' : 'pictures',
+					'album' : self.index,
+					'view' : 'small',
+					'name' : self.id,
+					'selected' : photo.index + PERPAGE * photo.page,
+					'albPg' : 0,
+					'qty' : self.length,
+					'fso' : 201,
+					'pView' : 'small',
+					'pg' : photo.page,
+					'selSet' : '1'}
+		cookie = sflyCookie('sflyImg', cdata)
+		self.__sc.setCookie(cookie)
+
+		request = urllib2.Request(SFApi.getdelphotourlbyid(self.id, 1))
+		response = self.__sc.opener.open(request)
+		buf = response.read()
+		response.close()
+		
+		if buf.find("failure") > -1:
+			raise ShutterflyException("Did not successfully delete photo")
+		
+		
+			
 	def __repr__(self):
 		return "<album %s : %s>" % (self.__id, self.__name)
 
 class ShutterflyPhoto(object):
+	__index = None
+	__page = None
 	__id = None
+	__url = None
 	__title = None
 	
+	index = property(lambda s: s.__index)
+	page = property(lambda s: s.__page)
+	id = property(lambda s: s.__id)
+	url = property(lambda s: s.__url)
 	title = property(lambda s: s.__title)
-	id = property(lambda s: s.__id)
 	
-	def __init__(self, id, title):
-		self.__id = id
-		self.__title = title
+	def __init__(self, index, page, id, title):
+		self.__index = index					# monotonically incrementing index (unique to page)
+		self.__page = page					# album page for photo
+		self.__id = utf8(id)					# unique photo id
+		self.__url = "http://im1.shutterfly.com/procserv/"; + id[:35] + '7' + id[36:]
+		self.__title = title					# photo title



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]