""" flickr.py Copyright 2004-6 James Clarke <james@jamesclarke.info> THIS SOFTWARE IS SUPPLIED WITHOUT WARRANTY OF ANY KIND, AND MAY BE COPIED, MODIFIED OR DISTRIBUTED IN ANY WAY, AS LONG AS THIS NOTICE AND ACKNOWLEDGEMENT OF AUTHORSHIP REMAIN. 2006-12-19 Applied patches from Berco Beute and Wolfram Kriesing. TODO list below is out of date! 2005-06-10 TOOD list: * flickr.blogs.* * flickr.contacts.getList * flickr.groups.browse * flickr.groups.getActiveList * flickr.people.getOnlineList * flickr.photos.getContactsPhotos * flickr.photos.getContactsPublicPhotos * flickr.photos.getContext * flickr.photos.getCounts * flickr.photos.getExif * flickr.photos.getNotInSet * flickr.photos.getPerms * flickr.photos.getRecent * flickr.photos.getUntagged * flickr.photos.setDates * flickr.photos.setPerms * flickr.photos.licenses.* * flickr.photos.notes.* * flickr.photos.transform.* * flickr.photosets.getContext * flickr.photosets.orderSets * flickr.reflection.* (not important) * flickr.tags.getListPhoto * flickr.urls.* """ __author__ = "James Clarke <james@jamesclarke.info>" __version__ = "$Rev$" __date__ = "$Date$" __copyright__ = "Copyright 2004-6 James Clarke" from urllib import urlencode, urlopen from xml.dom import minidom HOST = 'http://flickr.com' API = '/services/rest' #set these here or using flickr.API_KEY in your application API_KEY = '' email = None password = None class FlickrError(Exception): pass class Photo(object): """Represents a Flickr Photo.""" __readonly = ['id', 'secret', 'server', 'isfavorite', 'license', 'rotation', 'owner', 'dateposted', 'datetaken', 'takengranularity', 'title', 'description', 'ispublic', 'isfriend', 'isfamily', 'cancomment', 'canaddmeta', 'comments', 'tags', 'permcomment', 'permaddmeta'] #XXX: Hopefully None won't cause problems def __init__(self, id, owner=None, dateuploaded=None, \ title=None, description=None, ispublic=None, \ isfriend=None, isfamily=None, cancomment=None, \ canaddmeta=None, comments=None, tags=None, secret=None, \ isfavorite=None, server=None, license=None, rotation=None): """Must specify id, rest is optional.""" self.__loaded = False self.__cancomment = cancomment self.__canaddmeta = canaddmeta self.__comments = comments self.__dateuploaded = dateuploaded self.__description = description self.__id = id self.__license = license self.__isfamily = isfamily self.__isfavorite = isfavorite self.__isfriend = isfriend self.__ispublic = ispublic self.__owner = owner self.__rotation = rotation self.__secret = secret self.__server = server self.__tags = tags self.__title = title self.__dateposted = None self.__datetaken = None self.__takengranularity = None self.__permcomment = None self.__permaddmeta = None def __setattr__(self, key, value): if key in self.__class__.__readonly: raise AttributeError("The attribute %s is read-only." % key) else: super(Photo, self).__setattr__(key, value) def __getattr__(self, key): if not self.__loaded: self._load_properties() if key in self.__class__.__readonly: return super(Photo, self).__getattribute__("_%s__%s" % (self.__class__.__name__, key)) else: return super(Photo, self).__getattribute__(key) def _load_properties(self): """Loads the properties from Flickr.""" self.__loaded = True method = 'flickr.photos.getInfo' data = _doget(method, photo_id=self.id) photo = data.rsp.photo self.__secret = photo.secret self.__server = photo.server self.__isfavorite = photo.isfavorite self.__license = photo.license self.__rotation = photo.rotation owner = photo.owner self.__owner = User(owner.nsid, username=owner.username,\ realname=owner.realname,\ location=owner.location) self.__title = photo.title.text self.__description = photo.description.text self.__ispublic = photo.visibility.ispublic self.__isfriend = photo.visibility.isfriend self.__isfamily = photo.visibility.isfamily self.__dateposted = photo.dates.posted self.__datetaken = photo.dates.taken self.__takengranularity = photo.dates.takengranularity self.__cancomment = photo.editability.cancomment self.__canaddmeta = photo.editability.canaddmeta self.__comments = photo.comments.text try: self.__permcomment = photo.permissions.permcomment self.__permaddmeta = photo.permissions.permaddmeta except AttributeError: self.__permcomment = None self.__permaddmeta = None #TODO: Implement Notes? if hasattr(photo.tags, "tag"): if isinstance(photo.tags.tag, list): self.__tags = [Tag(tag.id, User(tag.author), tag.raw, tag.text) \ for tag in photo.tags.tag] else: tag = photo.tags.tag self.__tags = [Tag(tag.id, User(tag.author), tag.raw, tag.text)] def __str__(self): return '<Flickr Photo %s>' % self.id def setTags(self, tags): """Set the tags for current photo to list tags. (flickr.photos.settags) """ method = 'flickr.photos.setTags' tags = uniq(tags) _dopost(method, auth=True, photo_id=self.id, tags=tags) self._load_properties() def addTags(self, tags): """Adds the list of tags to current tags. (flickr.photos.addtags) """ method = 'flickr.photos.addTags' if isinstance(tags, list): tags = uniq(tags) _dopost(method, auth=True, photo_id=self.id, tags=tags) #load properties again self._load_properties() def removeTag(self, tag): """Remove the tag from the photo must be a Tag object. (flickr.photos.removeTag) """ method = 'flickr.photos.removeTag' tag_id = '' try: tag_id = tag.id except AttributeError: raise FlickrError, "Tag object expected" _dopost(method, auth=True, photo_id=self.id, tag_id=tag_id) self._load_properties() def setMeta(self, title=None, description=None): """Set metadata for photo. (flickr.photos.setMeta)""" method = 'flickr.photos.setMeta' if title is None: title = self.title if description is None: description = self.description _dopost(method, auth=True, title=title, \ description=description, photo_id=self.id) self.__title = title self.__description = description def getURL(self, size='Medium', urlType='url'): """Retrieves a url for the photo. (flickr.photos.getSizes) urlType - 'url' or 'source' 'url' - flickr page of photo 'source' - image file """ method = 'flickr.photos.getSizes' data = _doget(method, photo_id=self.id) for psize in data.rsp.sizes.size: if psize.label == size: return getattr(psize, urlType) raise FlickrError, "No URL found" def getSizes(self): """ Get all the available sizes of the current image, and all available data about them. Returns: A list of dicts with the size data. """ method = 'flickr.photos.getSizes' data = _doget(method, photo_id=self.id) ret = [] # The given props are those that we return and the according types, since # return width and height as string would make "75">"100" be True, which # is just error prone. props = {'url':str,'width':int,'height':int,'label':str,'source':str,'text':str} for psize in data.rsp.sizes.size: d = {} for prop,convert_to_type in props.items(): d[prop] = convert_to_type(getattr(psize, prop)) ret.append(d) return ret #def getExif(self): #method = 'flickr.photos.getExif' #data = _doget(method, photo_id=self.id) #ret = [] #for exif in data.rsp.photo.exif: #print exif.label, dir(exif) ##ret.append({exif.label:exif.}) #return ret ##raise FlickrError, "No URL found" def getLocation(self): """ Return the latitude+longitutde of the picture. Returns None if no location given for this pic. """ method = 'flickr.photos.geo.getLocation' try: data = _doget(method, photo_id=self.id) except FlickrError: # Some other error might have occured too!? return None loc = data.rsp.photo.location return [loc.latitude, loc.longitude] class Photoset(object): """A Flickr photoset.""" def __init__(self, id, title, primary, photos=0, description='', \ secret='', server=''): self.__id = id self.__title = title self.__primary = primary self.__description = description self.__count = photos self.__secret = secret self.__server = server id = property(lambda self: self.__id) title = property(lambda self: self.__title) description = property(lambda self: self.__description) primary = property(lambda self: self.__primary) def __len__(self): return self.__count def __str__(self): return '<Flickr Photoset %s>' % self.id def getPhotos(self): """Returns list of Photos.""" method = 'flickr.photosets.getPhotos' data = _doget(method, photoset_id=self.id) photos = data.rsp.photoset.photo p = [] for photo in photos: p.append(Photo(photo.id, title=photo.title, secret=photo.secret, \ server=photo.server)) return p def editPhotos(self, photos, primary=None): """Edit the photos in this set. photos - photos for set primary - primary photo (if None will used current) """ method = 'flickr.photosets.editPhotos' if primary is None: primary = self.primary ids = [photo.id for photo in photos] if primary.id not in ids: ids.append(primary.id) _dopost(method, auth=True, photoset_id=self.id,\ primary_photo_id=primary.id, photo_ids=ids) self.__count = len(ids) return True def addPhoto(self, photo): """Add a photo to this set. photo - the photo """ method = 'flickr.photosets.addPhoto' _dopost(method, auth=True, photoset_id=self.id, photo_id=photo.id) self.__count += 1 return True def removePhoto(self, photo): """Remove the photo from this set. photo - the photo """ method = 'flickr.photosets.removePhoto' _dopost(method, auth=True, photoset_id=self.id, photo_id=photo.id) self.__count = self.__count - 1 return True def editMeta(self, title=None, description=None): """Set metadata for photo. (flickr.photos.setMeta)""" method = 'flickr.photosets.editMeta' if title is None: title = self.title if description is None: description = self.description _dopost(method, auth=True, title=title, \ description=description, photoset_id=self.id) self.__title = title self.__description = description return True #XXX: Delete isn't handled well as the python object will still exist def delete(self): """Deletes the photoset. """ method = 'flickr.photosets.delete' _dopost(method, auth=True, photoset_id=self.id) return True def create(cls, photo, title, description=''): """Create a new photoset. photo - primary photo """ if not isinstance(photo, Photo): raise TypeError, "Photo expected" method = 'flickr.photosets.create' data = _dopost(method, auth=True, title=title,\ description=description,\ primary_photo_id=photo.id) set = Photoset(data.rsp.photoset.id, title, Photo(photo.id), photos=1, description=description) return set create = classmethod(create) class User(object): """A Flickr user.""" def __init__(self, id, username=None, isadmin=None, ispro=None, \ realname=None, location=None, firstdate=None, count=None): """id required, rest optional.""" self.__loaded = False #so we don't keep loading data self.__id = id self.__username = username self.__isadmin = isadmin self.__ispro = ispro self.__realname = realname self.__location = location self.__photos_firstdate = firstdate self.__photos_count = count #property fu id = property(lambda self: self._general_getattr('id')) username = property(lambda self: self._general_getattr('username')) isadmin = property(lambda self: self._general_getattr('isadmin')) ispro = property(lambda self: self._general_getattr('ispro')) realname = property(lambda self: self._general_getattr('realname')) location = property(lambda self: self._general_getattr('location')) photos_firstdate = property(lambda self: \ self._general_getattr('photos_firstdate')) photos_firstdatetaken = property(lambda self: \ self._general_getattr\ ('photos_firstdatetaken')) photos_count = property(lambda self: \ self._general_getattr('photos_count')) icon_server= property(lambda self: self._general_getattr('icon_server')) icon_url= property(lambda self: self._general_getattr('icon_url')) def _general_getattr(self, var): """Generic get attribute function.""" if getattr(self, "_%s__%s" % (self.__class__.__name__, var)) is None \ and not self.__loaded: self._load_properties() return getattr(self, "_%s__%s" % (self.__class__.__name__, var)) def _load_properties(self): """Load User properties from Flickr.""" method = 'flickr.people.getInfo' data = _doget(method, user_id=self.__id) self.__loaded = True person = data.rsp.person self.__isadmin = person.isadmin self.__ispro = person.ispro self.__icon_server = person.iconserver if int(person.iconserver) > 0: self.__icon_url = 'http://photos%s.flickr.com/buddyicons/%s.jpg' \ % (person.iconserver, self.__id) else: self.__icon_url = 'http://www.flickr.com/images/buddyicon.jpg' self.__username = person.username.text self.__realname = person.realname.text self.__location = person.location.text self.__photos_firstdate = person.photos.firstdate.text self.__photos_firstdatetaken = person.photos.firstdatetaken.text self.__photos_count = person.photos.count.text def __str__(self): return '<Flickr User %s>' % self.id def getPhotosets(self): """Returns a list of Photosets.""" method = 'flickr.photosets.getList' data = _doget(method, user_id=self.id) sets = [] if isinstance(data.rsp.photosets.photoset, list): for photoset in data.rsp.photosets.photoset: sets.append(Photoset(photoset.id, photoset.title.text,\ Photo(photoset.primary),\ secret=photoset.secret, \ server=photoset.server, \ description=photoset.description.text, photos=photoset.photos)) else: photoset = data.rsp.photosets.photoset sets.append(Photoset(photoset.id, photoset.title.text,\ Photo(photoset.primary),\ secret=photoset.secret, \ server=photoset.server, \ description=photoset.description.text, photos=photoset.photos)) return sets def getPublicFavorites(self, per_page='', page=''): return favorites_getPublicList(user_id=self.id, per_page=per_page, \ page=page) def getFavorites(self, per_page='', page=''): return favorites_getList(user_id=self.id, per_page=per_page, \ page=page) class Group(object): """Flickr Group Pool""" def __init__(self, id, name=None, members=None, online=None,\ privacy=None, chatid=None, chatcount=None): self.__loaded = False self.__id = id self.__name = name self.__members = members self.__online = online self.__privacy = privacy self.__chatid = chatid self.__chatcount = chatcount self.__url = None id = property(lambda self: self._general_getattr('id')) name = property(lambda self: self._general_getattr('name')) members = property(lambda self: self._general_getattr('members')) online = property(lambda self: self._general_getattr('online')) privacy = property(lambda self: self._general_getattr('privacy')) chatid = property(lambda self: self._general_getattr('chatid')) chatcount = property(lambda self: self._general_getattr('chatcount')) def _general_getattr(self, var): """Generic get attribute function.""" if getattr(self, "_%s__%s" % (self.__class__.__name__, var)) is None \ and not self.__loaded: self._load_properties() return getattr(self, "_%s__%s" % (self.__class__.__name__, var)) def _load_properties(self): """Loads the properties from Flickr.""" method = 'flickr.groups.getInfo' data = _doget(method, group_id=self.id) self.__loaded = True group = data.rsp.group self.__name = photo.name.text self.__members = photo.members.text self.__online = photo.online.text self.__privacy = photo.privacy.text self.__chatid = photo.chatid.text self.__chatcount = photo.chatcount.text def __str__(self): return '<Flickr Group %s>' % self.id def getPhotos(self, tags='', per_page='', page=''): """Get a list of photo objects for this group""" method = 'flickr.groups.pools.getPhotos' data = _doget(method, group_id=self.id, tags=tags,\ per_page=per_page, page=page) photos = [] for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) return photos def add(self, photo): """Adds a Photo to the group""" method = 'flickr.groups.pools.add' _dopost(method, auth=True, photo_id=photo.id, group_id=self.id) return True def remove(self, photo): """Remove a Photo from the group""" method = 'flickr.groups.pools.remove' _dopost(method, auth=True, photo_id=photo.id, group_id=self.id) return True class Tag(object): def __init__(self, id, author, raw, text): self.id = id self.author = author self.raw = raw self.text = text def __str__(self): return '<Flickr Tag %s (%s)>' % (self.id, self.text) #Flickr API methods #see api docs http://www.flickr.com/services/api/ #for details of each param #XXX: Could be Photo.search(cls) def photos_search(user_id='', auth=False, tags='', tag_mode='', text='',\ min_upload_date='', max_upload_date='',\ min_taken_date='', max_taken_date='', \ license='', per_page='', page='', sort=''): """Returns a list of Photo objects. If auth=True then will auth the user. Can see private etc """ method = 'flickr.photos.search' data = _doget(method, auth=auth, user_id=user_id, tags=tags, text=text,\ min_upload_date=min_upload_date,\ max_upload_date=max_upload_date, \ min_taken_date=min_taken_date, \ max_taken_date=max_taken_date, \ license=license, per_page=per_page,\ page=page, sort=sort) photos = [] if isinstance(data.rsp.photos.photo, list): for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) else: photos = [_parse_photo(data.rsp.photos.photo)] return photos #XXX: Could be class method in User def people_findByEmail(email): """Returns User object.""" method = 'flickr.people.findByEmail' data = _doget(method, find_email=email) user = User(data.rsp.user.id, username=data.rsp.user.username.text) return user def people_findByUsername(username): """Returns User object.""" method = 'flickr.people.findByUsername' data = _doget(method, username=username) user = User(data.rsp.user.id, username=data.rsp.user.username.text) return user #XXX: Should probably be in User as a list User.public def people_getPublicPhotos(user_id, per_page='', page=''): """Returns list of Photo objects.""" method = 'flickr.people.getPublicPhotos' data = _doget(method, user_id=user_id, per_page=per_page, page=page) photos = [] if hasattr(data.rsp.photos, "photo"): # Check if there are photos at all (may be been paging too far). if isinstance(data.rsp.photos.photo, list): for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) else: photos = [_parse_photo(data.rsp.photos.photo)] return photos #XXX: These are also called from User def favorites_getList(user_id='', per_page='', page=''): """Returns list of Photo objects.""" method = 'flickr.favorites.getList' data = _doget(method, auth=True, user_id=user_id, per_page=per_page,\ page=page) photos = [] if isinstance(data.rsp.photos.photo, list): for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) else: photos = [_parse_photo(data.rsp.photos.photo)] return photos def favorites_getPublicList(user_id, per_page='', page=''): """Returns list of Photo objects.""" method = 'flickr.favorites.getPublicList' data = _doget(method, auth=False, user_id=user_id, per_page=per_page,\ page=page) photos = [] if isinstance(data.rsp.photos.photo, list): for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) else: photos = [_parse_photo(data.rsp.photos.photo)] return photos def favorites_add(photo_id): """Add a photo to the user's favorites.""" method = 'flickr.favorites.add' _dopost(method, auth=True, photo_id=photo_id) return True def favorites_remove(photo_id): """Remove a photo from the user's favorites.""" method = 'flickr.favorites.remove' _dopost(method, auth=True, photo_id=photo_id) return True def groups_getPublicGroups(): """Get a list of groups the auth'd user is a member of.""" method = 'flickr.groups.getPublicGroups' data = _doget(method, auth=True) groups = [] if isinstance(data.rsp.groups.group, list): for group in data.rsp.groups.group: groups.append(Group(group.id, name=group.name)) else: group = data.rsp.groups.group groups = [Group(group.id, name=group.name)] return groups def groups_pools_getGroups(): """Get a list of groups the auth'd user can post photos to.""" method = 'flickr.groups.pools.getGroups' data = _doget(method, auth=True) groups = [] if isinstance(data.rsp.groups.group, list): for group in data.rsp.groups.group: groups.append(Group(group.id, name=group.name, \ privacy=group.privacy)) else: group = data.rsp.groups.group groups = [Group(group.id, name=group.name, privacy=group.privacy)] return groups def tags_getListUser(user_id=''): """Returns a list of tags for the given user (in string format)""" method = 'flickr.tags.getListUser' auth = user_id == '' data = _doget(method, auth=auth, user_id=user_id) if isinstance(data.rsp.tags.tag, list): return [tag.text for tag in data.rsp.tags.tag] else: return [data.rsp.tags.tag.text] def tags_getListUserPopular(user_id='', count=''): """Gets the popular tags for a user in dictionary form tag=>count""" method = 'flickr.tags.getListUserPopular' auth = user_id == '' data = _doget(method, auth=auth, user_id=user_id) result = {} if isinstance(data.rsp.tags.tag, list): for tag in data.rsp.tags.tag: result[tag.text] = tag.count else: result[data.rsp.tags.tag.text] = data.rsp.tags.tag.count return result def tags_getrelated(tag): """Gets the related tags for given tag.""" method = 'flickr.tags.getRelated' data = _doget(method, auth=False, tag=tag) if isinstance(data.rsp.tags.tag, list): return [tag.text for tag in data.rsp.tags.tag] else: return [data.rsp.tags.tag.text] def contacts_getPublicList(user_id): """Gets the contacts (Users) for the user_id""" method = 'flickr.contacts.getPublicList' data = _doget(method, auth=False, user_id=user_id) if isinstance(data.rsp.contacts.contact, list): return [User(user.nsid, username=user.username) \ for user in data.rsp.contacts.contact] else: user = data.rsp.contacts.contact return [User(user.nsid, username=user.username)] def interestingness(): method = 'flickr.interestingness.getList' data = _doget(method) photos = [] if isinstance(data.rsp.photos.photo , list): for photo in data.rsp.photos.photo: photos.append(_parse_photo(photo)) else: photos = [_parse_photo(data.rsp.photos.photo)] return photos def test_login(): method = 'flickr.test.login' data = _doget(method, auth=True) user = User(data.rsp.user.id, username=data.rsp.user.username.text) return user def test_echo(): method = 'flickr.test.echo' data = _doget(method) return data.rsp.stat #useful methods def _doget(method, auth=False, **params): #uncomment to check you aren't killing the flickr server #print "***** do get %s" % method #convert lists to strings with ',' between items for (key, value) in params.items(): if isinstance(value, list): params[key] = ','.join([item for item in value]) url = '%s%s/?api_key=%s&method=%s&%s'% \ (HOST, API, API_KEY, method, urlencode(params)) if auth: url = url + '&email=%s&password=%s' % (email, password) #another useful debug print statement #print url xml = minidom.parse(urlopen(url)) data = unmarshal(xml) if not data.rsp.stat == 'ok': msg = "ERROR [%s]: %s" % (data.rsp.err.code, data.rsp.err.msg) raise FlickrError, msg return data def _dopost(method, auth=False, **params): #uncomment to check you aren't killing the flickr server #print "***** do post %s" % method #convert lists to strings with ',' between items for (key, value) in params.items(): if isinstance(value, list): params[key] = ','.join([item for item in value]) url = '%s%s/' % (HOST, API) payload = 'api_key=%s&method=%s&%s'% \ (API_KEY, method, urlencode(params)) if auth: payload = payload + '&email=%s&password=%s' % (email, password) #another useful debug print statement #print url #print payload xml = minidom.parse(urlopen(url, payload)) data = unmarshal(xml) if not data.rsp.stat == 'ok': msg = "ERROR [%s]: %s" % (data.rsp.err.code, data.rsp.err.msg) raise FlickrError, msg return data def _parse_photo(photo): """Create a Photo object from photo data.""" owner = User(photo.owner) title = photo.title ispublic = photo.ispublic isfriend = photo.isfriend isfamily = photo.isfamily secret = photo.secret server = photo.server p = Photo(photo.id, owner=owner, title=title, ispublic=ispublic,\ isfriend=isfriend, isfamily=isfamily, secret=secret, \ server=server) return p #stolen methods class Bag: pass #unmarshal taken and modified from pyamazon.py #makes the xml easy to work with def unmarshal(element): rc = Bag() if isinstance(element, minidom.Element): for key in element.attributes.keys(): setattr(rc, key, element.attributes[key].value) childElements = [e for e in element.childNodes \ if isinstance(e, minidom.Element)] if childElements: for child in childElements: key = child.tagName if hasattr(rc, key): if type(getattr(rc, key)) <> type([]): setattr(rc, key, [getattr(rc, key)]) setattr(rc, key, getattr(rc, key) + [unmarshal(child)]) elif isinstance(child, minidom.Element) and \ (child.tagName == 'Details'): # make the first Details element a key setattr(rc,key,[unmarshal(child)]) #dbg: because otherwise 'hasattr' only tests #dbg: on the second occurence: if there's a #dbg: single return to a query, it's not a #dbg: list. This module should always #dbg: return a list of Details objects. else: setattr(rc, key, unmarshal(child)) else: #jec: we'll have the main part of the element stored in .text #jec: will break if tag <text> is also present text = "".join([e.data for e in element.childNodes \ if isinstance(e, minidom.Text)]) setattr(rc, 'text', text) return rc #unique items from a list from the cookbook def uniq(alist): # Fastest without order preserving set = {} map(set.__setitem__, alist, []) return set.keys() if __name__ == '__main__': print test_echo()