"""
Shutterfly data API
"""
import urllib, urllib2
import cookielib
from urlparse import urlparse
from cgi import parse_qs
import re
import time
import os
from gettext import gettext as _
FORMAT_STRING = _("%Y-%m-%d %H:%M:%S")
PERPAGE = 80
def utf8(v):
""" ensure to get 'v' in an UTF8 encoding (respect None) """
if v != None:
if type(v) != unicode:
v = unicode(v, "utf_8", "replace")
v = v.encode("utf_8")
return v
def mkRequest(url, data=None, headers={}):
""" create a urllib2.Request """
if data:
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)
00067 class ShutterflyException(Exception):
"""
Web exception
"""
pass
def encode_multipart_formdata(fields, files):
"""
fields is a sequence of (name, value) elements for regular form fields.
files is a sequence of (name, filename, value) elements for data to be uploaded as files
Return (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = "END_OF_PART"
CRLF = '\r\n'
L = []
for (key, value) in fields:
L.append('--' + BOUNDARY)
L.append('Content-Disposition: form-data; name="%s"' % key)
L.append('')
L.append(value)
for (filename, mimeType, value) in files:
L.append('--' + BOUNDARY)
L.append('Content-Disposition: form-data; name="Image.Data"; filename="%s"' % (filename))
L.append('Content-Type: %s' % mimeType)
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
class ShutterflyHTTPErrorProcessor(urllib2.HTTPErrorProcessor):
def http_response(self, request, response):
return urllib2.HTTPErrorProcessor.http_response(self, request, response)
https_response = http_response
class SFApi:
accounturl = "http://www.shutterfly.com/account/acc_info.jsp"
entryurl = "http://www.shutterfly.com/signin/viewSignin.sfly"
loginurl = "https://www.shutterfly.com/signin/signin.sfly"
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"
def _getaccounturl():
return SFApi.accounturl
def _getentryurl():
return SFApi.entryurl
def _getloginurl():
return SFApi.loginurl
def _getalbumurlbypage(page):
return SFApi.albumurl % {"page" : page}
def _getaddalbumurl():
return SFApi.addalbumurl
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
def _getuploadimageurl():
return SFApi.uploadimageurl
getaccounturl = staticmethod(_getaccounturl)
getentryurl = staticmethod(_getentryurl)
getloginurl = staticmethod(_getloginurl)
getalbumurlbypage = staticmethod(_getalbumurlbypage)
getaddalbumurl = staticmethod(_getaddalbumurl)
getphotourlbyid = staticmethod(_getphotourlbyid)
getdelphotourlbyid = staticmethod(_getdelphotourlbyid)
getuploadurl = staticmethod(_getuploadurl)
getuploadimageurl = staticmethod(_getuploadimageurl)
00165 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
class ShutterflyConnection(object):
__user = None
__fid = None
__cj = None
__opener = None
user = property(lambda s: s.__user)
fid = property(lambda s: s.__fid)
opener = property(lambda s: s.__opener)
def __init__(self, user, password):
"""
Check if the user is already connected
Connect to the shutterly entrance page to aquire a FID from
the response url
Login using credentials and aquired FID
"""
self.__cj = cookielib.CookieJar()
self.__opener = urllib2.build_opener(ShutterflyCookieProcessor(self.__cj))
urllib2.install_opener(self.opener)
user = utf8(user)
password = utf8(password)
self.__user = user
request = mkRequest(SFApi.getaccounturl())
response = self.opener.open(request)
buf = response.read()
response.close()
if buf.find(user) > 0:
return
request = mkRequest(SFApi.getentryurl())
response = self.opener.open(request)
self.__fid = parse_qs(urlparse(response.geturl())[4])['fid'][0]
headers = {"Content-type" : "application/x-www-form-urlencoded", }
data = {"userName" : user,
"password" : password,
"_rememberUserName" : "off",
"fid" : self.__fid, }
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)
class Shutterfly:
def __init__(self, user, password):
""" Create a Shutterfly instance """
self.__sc = ShutterflyConnection(user, password)
def getAlbums(self):
"""
Get a dictionary of available Shutterfly albums on this account
"""
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?)")
if buf.find("var status='notLoggedIn'") > -1:
raise ShutterflyException("No longer logged in (timeout, logged in elsewhere?)")
page = 1
count = int(re.search("totalPics\s*=\s*(\d+)", buf).group(1))
perpage = int(re.search("picsThisPage\s*=\s*(\d+)", buf).group(1))
l = {}
while count > 0:
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
if count > 0:
request = mkRequest(SFApi.getalbumurlbypage(page))
response = self.__sc.opener.open(request)
buf = response.read()
response.close()
return l
def createAlbum(self, name, description=""):
"""
Create an album on Shutterfly and return the ShutterflyAlbum instance
Need to retrieve AuthID before uploading (adding albums)
"""
name = utf8(name)
headers = {"Content-type" : "application/x-www-form-urlencoded", }
data = {"albumTitle" : name,
"albumDesc" : description,
"createAlbum" : "1", }
request = mkRequest(SFApi.getaddalbumurl(), data, headers)
response = self.__sc.opener.open(request)
buf = response.read()
response.close()
albums = self.getAlbums()
return albums[name]
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)
length = property(lambda s: s.__length)
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.__length = length
def getPhotos(self):
"""
Get a dictionary of available photos in this album
"""
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?)")
if buf.find("var status='notLoggedIn'") > -1:
raise ShutterflyException("No longer logged in (timeout, logged in elsewhere?)")
page = 1
count = int(re.search("totalPics\s*=\s*(\d+)", buf).group(1))
perpage = int(re.search("picsThisPage\s*=\s*(\d+)", buf).group(1))
l = {}
while count > 0:
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
if count > 0:
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, name, description = ""):
"""
Upload a photo to this album
"""
filename = utf8(filename)
if os.path.isfile(filename):
request = mkRequest(SFApi.getuploadurl())
response = self.__sc.opener.open(request)
buf = response.read()
response.close()
data = []
for item in ['ProtocolVersion', 'RequestType', 'AuthenticationID',
'PartnerID', 'PartnerSubID', 'previewURL', 'redirect',
'doNotDisplayFormAfterUpload']:
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,
[(name, mimeType, open(filename, "rb").read())])
headers = {"Content-Type" : content_type,
"Content-Length" : str(len(body)), }
request = urllib2.Request(SFApi.getuploadimageurl(),
body, headers)
response = self.__sc.opener.open(request)
buf = response.read()
response.close()
if response.geturl().find("Success=1") == -1:
raise ShutterflyException("Could not add photo")
photoid = re.search('name="vcidList" value="(.*)"', buf).group(1)
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):
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)
def __init__(self, index, page, id, title):
self.__index = index
self.__page = page
self.__id = utf8(id)
self.__url = "http://im1.shutterfly.com/procserv/" + id[:35] + '7' + id[36:]
self.__title = title