Source

django-syncr / syncr / app / picasaweb.py

Full commit
#!/usr/bin/python
# -*- coding: utf-8 -*-
# vim: ai ts=4 sw=4 et
"""
TODO:
- finish the couple of methods that are not working yet
 - favorites
 - sync recent
"""

import calendar
from datetime import datetime, timedelta
from time import strptime
import math
import gdata.photos.service
import gdata.media
import gdata.geo
from django.core.exceptions import ObjectDoesNotExist
from syncr.picasaweb.models import Photo, Album, FavoriteList


class PicasawebSyncrError(Exception):

    pass


class PicasawebSyncr:

    """PicasaSyncr objects sync picasaweb photos, photo sets, and favorites
    lists with the Django backend.

    It does not currently sync user meta-data. Photo, PhotoSet, and
    FavoriteList objects include some meta-data, but are mostly Django
    ManyToManyFields to Photo objects.

    This app requires google gdata library. Available at:
    http://code.google.com/p/gdata-python-client/downloads/list
    """

    def __init__(self, email, password, cli_verbose=False):
        """Construct a new PicasawebSyncr object.

        Required arguments
          email: a google email (username)
          password: account password
        """

        self.cli_verbose = cli_verbose
        self.email = email
        gd_client = gdata.photos.service.PhotosService()
        gd_client.email = email
        gd_client.password = password
        gd_client.source = 'django-syncr-picasaweb'
        gd_client.ProgrammaticLogin()
        self.gd_client = gd_client

    def getExifKey(self, exif_data, key):
        try:
            return exif_data[key]
        except:
            return ''

    def getPhotoTagList(self, username, albumname, gphoto_id):
        if self.cli_verbose:
            print '>>> getPhotoTagList', username, albumname, gphoto_id
        feed = \
            self.gd_client.GetFeed('/data/feed/api/user/%s/album/%s/photoid/%s?kind=tag'
                                    % (username, albumname, gphoto_id))
        return ' '.join(entry.title.text for entry in feed.entry)

    def _syncPhoto(self, photo_entry, username, albumname=None, refresh=False):
        """Synchronize a picasaweb photo with the Django backend.
        Required Arguments
          photo_entry: A google data api photo entry
        """

        gphoto_id = photo_entry.gphoto_id.text

        # if self.cli_verbose:
        #    print "syncPhoto album %s id %s" % (albumname, gphoto_id)
        # if we're refreshing this data, then delete the Photo first...

        if refresh:
            try:
                p = Photo.objects.get(gphoto_id=gphoto_id)
                p.delete()
            except ObjectDoesNotExist:
                pass

        if albumname is None:
            if self.cli_verbose:
                print 'Albumname unknown getAlbumFeed for', username
            feed = self.getAlbumFeed(username=username)
            album_id = photo_entry.albumid.text
            albums = [album for album in feed.entry
                      if album.gphoto_id.text == album_id]
            if len(albums) != 1:
                raise PicasawebSyncrError('No such album found for: %s'
                         % album_or_id)
            albumname = albums[0].name.text

        updated = datetime(*strptime(photo_entry.updated.text[:-4]
                            + '000Z', '%Y-%m-%dT%H:%M:%S.000Z')[:7])
        try:
            geo_latitude = photo_entry.geo.latitude()
            geo_longtitude = photo_entry.geo.longtitude()
        except:
            geo_latitude = ''
            geo_longtitude = ''

        default_dict = {  # 'square_url': urls['Square'],
                          # 'license': photo_xml.photo[0]['license'],
                          # 'exif_orientation': photo_entry.exif.,
                          # 'exif_software': photo_entry.exif.,
                          # 'exif_aperture': photo_entry.exif.,
                          # 'exif_metering_mode': ,
            'updated': updated,
            'gphoto_id': gphoto_id,
            'owner': username,
            'title': photo_entry.title.text,
            'description': photo_entry.summary.text or '',
            'taken_date': datetime(*strptime(photo_entry.timestamp.isoformat()[:-4]
                                    + '000Z', '%Y-%m-%dT%H:%M:%S.000Z'
                                   )[:7]),
            'photopage_url': photo_entry.GetAlternateLink().href,
            'small_url': photo_entry.media.thumbnail[0].url,
            'medium_url': photo_entry.media.thumbnail[1].url,
            'thumbnail_url': photo_entry.media.thumbnail[2].url,
            'content_url': photo_entry.media.content[0].url,
            'tags': photo_entry.media.keywords.text,
            'geo_latitude': geo_latitude,
            'geo_longitude': geo_longtitude,
            'exif_model': photo_entry.exif.model\
                 and photo_entry.exif.model.text or '',
            'exif_make': photo_entry.exif.make\
                 and photo_entry.exif.make.text or '',
            'exif_exposure': photo_entry.exif.exposure\
                 and photo_entry.exif.exposure.text or '',
            'exif_iso': photo_entry.exif.iso\
                 and photo_entry.exif.iso.text or '',
            'exif_flash': photo_entry.exif.flash\
                 and photo_entry.exif.flash.text or '',
            'exif_focal_length': photo_entry.exif.focallength\
                 and photo_entry.exif.focallength.text or '',
            }

        # 'exif_color_space': self.getExifKey(exif_data, 'Color Space'),

        (obj, created) = \
            Photo.objects.get_or_create(gphoto_id=gphoto_id,
                defaults=default_dict)
        if self.cli_verbose:
            status = created and 'created' or 'already exists (same)'
            delta = timedelta(seconds=3)
            #if obj.updated < updated:
            if not created and (updated > (obj.updated + delta)):
                status = 'updated'
            print 'Photo %s: %s' % (obj, status)

        #if not created and obj.updated < updated:
        if not created and (updated > (obj.updated + delta)):
            for (key, value) in default_dict.items():
                setattr(obj, key, value)
            obj.save()
        return obj

    # TODO: syncPhoto

    # TODO: syncRecent

    def getAlbumFeed(self, username=None):
        ''' Retrieves the xml feed for a picasaweb user.'''
        #kwargs = {}
        #if username is not None:
        #    kwargs.setdefault('user', username)
        #feed = self.gd_client.GetUserFeed(**kwargs)
        if not username:
            return None
        feed = self.gd_client.GetUserFeed(user=username)
        return feed

    def syncAlbum(self, album, username=None):
        ''' Pull basic album info into local model, from the top-level feed.'''
        if isinstance(album, (int, long, str)):
            feed = self.getAlbumFeed(username=username)
            if isinstance(album, (int, long)):
                album_id = '%s' % album
                albums = [album for album in feed.entry
                          if album.gphoto_id.text == album_id]
            else:
                album_name = album
                albums = [album for album in feed.entry
                          if album.name.text == album_name]
            if len(albums) != 1:
                raise PicasawebSyncrError(
                    'No such album found for: %s (found %s)' % (album, albums))
            album = albums[0]

        #print "\nDEBUG: album = %s" % album # joe debug
        #print "\nDEBUG: album = %s" % dir(album) # joe debug
        #print "\nDEBUG: album = %s" % album.gphoto_id.text # joe debug
        #print "\nDEBUG: album.media = %s" % album.media # joe debug
        #print "\nDEBUG: album.media.thumbnail = %s" % dir(album.media.thumbnail[0])
        print "\nDEBUG: album thumbnail = %s" % album.media.thumbnail[0].url
        print "\nDEBUG: album %s x %s" % \
                (album.media.thumbnail[0].height,
                 album.media.thumbnail[0].width)


        updated = datetime(*strptime(album.updated.text[:-4] + '000Z',
                           '%Y-%m-%dT%H:%M:%S.000Z')[:7])
        gphoto_id = album.gphoto_id.text
        username = album.user.text
        nickname = album.nickname.text
        numphotos = int(album.numphotos.text)
        albumname = album.name.text
        location = album.location.text
        if location is None:
            location = ''
        default_dict = {
            'gphoto_id': gphoto_id,
            'albumname': albumname,
            'owner': username,
            'nickname': nickname,
            'title': album.title.text,
            'description': album.summary.text or '',
            'location': location,
            'updated': updated,
            'access': album.access.text,
            'thumbnail_url': album.media.thumbnail[0].url,
            'thumbnail_height': album.media.thumbnail[0].height,
            'thumbnail_width': album.media.thumbnail[0].width,
            }
        (d_album, created) = \
            Album.objects.get_or_create(gphoto_id=gphoto_id,
                defaults=default_dict)
        delta = timedelta(seconds=3)
        if self.cli_verbose:
            status = created and 'created' or 'already exists (same)'
            if not created and updated > (d_album.updated + delta):
                status = 'updated'
            print 'Album', album.title.text, status
        if not created and (updated > (d_album.updated + delta)):
            for (key, value) in default_dict.items():
                setattr(d_album, key, value)
            d_album.save()

    # get photo list

        feed_url = '/data/feed/api/user/%s/album/%s' % (username, albumname)
        feed_url += '?kind=photo'

    # if the PICASA_THUMBSIZES and PICASA_IMGMAX values are set in settings.py,
    # then include those settings in the feed url

        from django.conf import settings

    # PICASA_THUMBSIZES can be set in settings.py as a tuple of three
    # sizes. E.g. ( '72c', '160c', '288',)
    # This setting controls the sizes of the thumbnails provided

        if getattr(settings, 'PICASA_THUMBSIZES', False):
            feed_url += '&thumbsize=%s'\
                 % ','.join(settings.PICASA_THUMBSIZES)

    # PICASA_IMGMAX can be set in settings.py to allow content_url to be
    # used directly in an <img> tag if this is set to a size up to 800.

        if getattr(settings, 'PICASA_IMGMAX', False):
            feed_url += '&imgmax=%s' % settings.PICASA_IMGMAX

        photo_feed = self.gd_client.GetFeed(feed_url)

        for photo_entry in photo_feed.entry:
            photo = self._syncPhoto(photo_entry, username,
                                    albumname=albumname)
            if photo:
                d_album.photos.add(photo)
        feed_ids = set(photo_entry.gphoto_id.text for photo_entry in
                       photo_feed.entry)
        local_ids = set(e['gphoto_id'] for e in
                        d_album.photos.values('gphoto_id'))
        photo_diff = local_ids - feed_ids
        if self.cli_verbose:
            if photo_diff:
                print 'Sync deletes photos:', photo_diff
        for gphoto_id in photo_diff:
            photo = d_album.photos.get(gphoto_id=gphoto_id)
            photo.delete()

    def syncAllAlbums(self, username=None):
        """Synchronize all photo albums for the picasaweb user."""

        if self.cli_verbose:
            print 'Sync all albums for', username or self.email
        feed = self.getAlbumFeed(username=username)

        for album in feed.entry:
            self.syncAlbum(album)