Esempio n. 1
0
def delete_album_file(album_file, albumdirectory, msg, options):
    """sanity check - only delete from album directory."""
    if not album_file.startswith(albumdirectory):
        print >> sys.stderr, ("Internal error - attempting to delete file "
                              "that is not in album directory:\n    %s") % (
                                  su.fsenc(album_file))
        return False
    if msg:
        print "%s: %s" % (msg, su.fsenc(album_file))

    if not options.delete:
        if not options.dryrun:
            print "Invoke phoshare with the -d option to delete this file."
        return False
    if options.dryrun:
        return True

    try:
        if os.path.isdir(album_file):
            file_list = os.listdir(album_file)
            for subfile in file_list:
                delete_album_file(os.path.join(album_file, subfile),
                                  albumdirectory, msg, options)
            os.rmdir(album_file)
        else:
            os.remove(album_file)
        return True
    except OSError, ex:
        print >> sys.stderr, "Could not delete %s: %s" % (su.fsenc(album_file),
                                                          ex)
Esempio n. 2
0
def delete_album_file(album_file, albumdirectory, msg, options):
    """sanity check - only delete from album directory."""
    if not album_file.startswith(albumdirectory):
        print >> sys.stderr, (
            "Internal error - attempting to delete file "
            "that is not in album directory:\n    %s") % (su.fsenc(album_file))
        return False
    if msg:
        print "%s: %s" % (msg, su.fsenc(album_file))

    if not imageutils.should_delete(options):
        return False
    if options.dryrun:
        return True

    try:
        if os.path.isdir(album_file):
            file_list = os.listdir(album_file)
            for subfile in file_list:
                delete_album_file(os.path.join(album_file, subfile),
                                  albumdirectory, msg, options)
            os.rmdir(album_file)
        else:
            os.remove(album_file)
        return True
    except OSError, ex:
        print >> sys.stderr, "Could not delete %s: %s" % (su.fsenc(album_file),
                                                          ex)
Esempio n. 3
0
    def delete_online_album(self, album, msg, options):
        """Delete an online album."""
        album_name = su.unicode_string(album.title.text)
        if msg:
            print "%s: %s" % (msg, su.fsenc(album_name))

        if not options.delete:
            if not options.dryrun:
                print "Invoke Phoshare with the -d option to delete this album."
            return False
        if options.dryrun:
            return True

        if (self.confirm_manager.confirm(
                album_name, "Delete " + msg + " " + album_name + "? ", "ny") !=
                1):
            print >> sys.stderr, 'Not deleted because not confirmed.'
            return False

        try:
            self.client.throttle()
            self.client.gd_client.Delete(album)
            return True
        except gdata.photos.service.GooglePhotosException, e:
            print >> sys.stderr, "Could not delete %s: %s" % (
                su.fsenc(album_name), e)
Esempio n. 4
0
def check_media_update(client, picasa_photo, photo, export_name, options):
    """Checks if the media of an online photo needs to be updated, and
    performs the update if necessary.

    Args:
        client: PicasaWeb client.
        picasa_photo: handle to the PicasaWeb photo.
        photo: the IPhotoImage photo.
        export_name: name of image (for output messages).
        options: processing options.

    Returns:
        the picasa_photo handle (after the update).
    """
    needs_update = False

    picasa_updated = convert_atom_timestamp_to_epoch(picasa_photo.updated.text)
    if int(picasa_updated) < int(get_picasaweb_date(photo.mod_date)):
        print "Changed: %s: newer version is available: %s vs %s." % (
            su.fsenc(export_name),
            convert_picasaweb_date(picasa_updated),
            photo.mod_date,
        )
        needs_update = True
    else:
        file_updated = str(int(os.path.getmtime(photo.image_path) * 1000))
        if int(picasa_updated) < int(file_updated):
            print "Changed: %s: newer file is available: %s vs %s." % (
                su.fsenc(export_name),
                convert_picasaweb_date(picasa_updated),
                convert_picasaweb_date(file_updated),
            )
            needs_update = True
    # With creative renaming in iPhoto it is possible to get stale
    # files if titles get swapped between images. Double check the size,
    # allowing for some difference for meta data changes made in the
    # exported copy
    source_size = os.path.getsize(photo.image_path)
    export_size = int(picasa_photo.size.text)
    diff = abs(source_size - export_size)
    if diff > _MAX_FILE_DIFF:
        print str.format("Changed:  {:s}: file size: {:,d} vs. {:,d}", su.fsenc(export_name), export_size, source_size)
        needs_update = True
    elif diff != 0:
        if options.verbose:
            print str.format("Ignored:  {:s}: file size: {:,d} vs. {:,d}", export_name, export_size, source_size)

    if not needs_update:
        return picasa_photo
    if not options.update:
        print "Needs update: " + su.fsenc(export_name) + "."
        print "Use the -u option to update this file."
        return picasa_photo
    print ("Updating media: " + export_name)
    if options.dryrun:
        return picasa_photo
    client.throttle()
    return client.gd_client.UpdatePhotoBlob(
        picasa_photo, photo.image_path, content_type=get_content_type(photo.image_path)
    )
Esempio n. 5
0
def delete_online_photo(client, photo, album_name, msg, options):
    """Delete an online photo."""
    global _delete_limit
    if msg:
        print "%s: %s" % (msg, su.fsenc(album_name) + "/" + photo.title.text)

    if not options.delete:
        if not options.dryrun:
            print "Invoke Phoshare with the -d option to delete this image."
        return False
    if options.dryrun:
        return True

    if not _delete_limit:
        print "Too many images to be deleted - skipping..."
        return True
    _delete_limit -= 1

    try:
        client.throttle()
        client.gd_client.Delete(photo)
        return True
    except gdata.photos.service.GooglePhotosException, ex:
        print >> sys.stderr, "Could not delete %s: %s" % (su.fsenc(
            photo.title.text), ex)
        return False
Esempio n. 6
0
    def delete_online_album(self, album, msg, options):
        """Delete an online album."""
        album_name = su.unicode_string(album.title.text)
        if msg:
            print "%s: %s" % (msg, su.fsenc(album_name))

        if not options.delete:
            if not options.dryrun:
                print "Invoke Phoshare with the -d option to delete this album."
            return False
        if options.dryrun:
            return True
         
        if (self.confirm_manager.confirm(album_name, "Delete " + msg + " " +
                                         album_name + "? ", "ny") != 1):
            print >> sys.stderr, 'Not deleted because not confirmed.'
            return False

        try:
            self.client.throttle()
            self.client.gd_client.Delete(album)
            return True
        except gdata.photos.service.GooglePhotosException, e:
            print >> sys.stderr, "Could not delete %s: %s" % (
                su.fsenc(album_name), e)
Esempio n. 7
0
def delete_online_photo(client, photo, album_name, msg, options):
    """Delete an online photo."""
    global _delete_limit
    if msg:
        print "%s: %s" % (msg, su.fsenc(album_name) + "/" + photo.title.text)

    if not options.delete:
        if not options.dryrun:
            print "Invoke Phoshare with the -d option to delete this image."
        return False
    if options.dryrun:
        return True

    if not _delete_limit:
        print "Too many images to be deleted - skipping..."
        return True
    _delete_limit -= 1
    
    try:
        client.throttle()
        client.gd_client.Delete(photo)
        return True
    except gdata.photos.service.GooglePhotosException, ex:
        print >> sys.stderr, "Could not delete %s: %s" % (
            su.fsenc(photo.title.text), ex)
        return False
Esempio n. 8
0
def check_media_update(client, picasa_photo, photo, export_name, options):
    """Checks if the media of an online photo needs to be updated, and
    performs the update if necessary.

    Args:
        client: PicasaWeb client.
        picasa_photo: handle to the PicasaWeb photo.
        photo: the IPhotoImage photo.
        export_name: name of image (for output messages).
        options: processing options.

    Returns:
        the picasa_photo handle (after the update).
    """
    needs_update = False

    picasa_updated = convert_atom_timestamp_to_epoch(picasa_photo.updated.text)
    if (int(picasa_updated) < int(get_picasaweb_date(photo.mod_date))):
        print "Changed: %s: newer version is available: %s vs %s." % (
            su.fsenc(export_name), convert_picasaweb_date(picasa_updated),
            photo.mod_date)
        needs_update = True
    else:
        file_updated = str(int(os.path.getmtime(photo.image_path) * 1000))
        if int(picasa_updated) < int(file_updated):
            print "Changed: %s: newer file is available: %s vs %s." % (
                su.fsenc(export_name), convert_picasaweb_date(picasa_updated),
                convert_picasaweb_date(file_updated))
            needs_update = True
    # With creative renaming in iPhoto it is possible to get stale
    # files if titles get swapped between images. Double check the size,
    # allowing for some difference for meta data changes made in the
    # exported copy
    source_size = os.path.getsize(photo.image_path)
    export_size = int(picasa_photo.size.text)
    diff = abs(source_size - export_size)
    if diff > _MAX_FILE_DIFF:
        print str.format("Changed:  {:s}: file size: {:,d} vs. {:,d}",
                         su.fsenc(export_name), export_size, source_size)
        needs_update = True
    elif diff != 0:
        if options.verbose:
            print str.format("Ignored:  {:s}: file size: {:,d} vs. {:,d}",
                             export_name, export_size, source_size)

    if not needs_update:
        return picasa_photo
    if not options.update:
        print "Needs update: " + su.fsenc(export_name) + "."
        print "Use the -u option to update this file."
        return picasa_photo
    print("Updating media: " + export_name)
    if options.dryrun:
        return picasa_photo
    client.throttle()
    return client.gd_client.UpdatePhotoBlob(picasa_photo,
                                            photo.image_path,
                                            content_type=get_content_type(
                                                photo.image_path))
Esempio n. 9
0
    def load_album(self, client, online_albums, options):
        """Walks the album directory tree, and scans it for existing files."""

        if options.verbose:
            print 'Reading online album ' + self.name
        comments = self.iphoto_container.getcommentwithouthints().strip()
        album_date = self.iphoto_container.date
        if album_date:
            # Adjust to just a date
            album_date = datetime.datetime(album_date.year, album_date.month, album_date.day)
            timestamp = get_picasaweb_date(album_date)
        else:
            timestamp = None
        self.online_album = online_albums.get(self.name)
        if not self.online_album:
            print "Creating album: " + su.fsenc(self.name)
            if not options.dryrun: 
                client.throttle()
                self.online_album = client.gd_client.InsertAlbum(
                    title=self.name,
                    summary=comments,
                    access='private',
                    timestamp=timestamp)
                online_albums[self.name] = self.online_album
            return

        # Check the properties of the online album
        changed = False
        online_album_summary_text = su.unicode_string(
            self.online_album.summary.text)
        if online_album_summary_text != comments:
            print 'Updating summary for online album %s (%s vs. %s)' % (
                su.fsenc(self.name),
                su.fsenc(online_album_summary_text),
                su.fsenc(comments))
            self.online_album.summary.text = comments
            changed = True

        if (timestamp and timestamp != self.online_album.timestamp.text):
            print 'Updating timestamp for online album %s (%s/%s)' % (
                su.fsenc(self.name),
                self.online_album.timestamp.datetime(),
                album_date)
            self.online_album.timestamp.text = timestamp
            changed = True

        if changed and not options.dryrun:
            client.throttle()
            try:
                self.online_album = client.gd_client.Put(
                    self.online_album, 
                    self.online_album.GetEditLink().href,
                    converter=gdata.photos.AlbumEntryFromString)
            except gdata.photos.service.GooglePhotosException, e:
                print 'Failed to update data for online album %s: %s' % (
                    self.name, str(e))
Esempio n. 10
0
    def load_album(self, client, online_albums, options):
        """Walks the album directory tree, and scans it for existing files."""

        if options.verbose:
            print 'Reading online album ' + self.name
        comments = self.iphoto_container.getcommentwithouthints().strip()
        album_date = self.iphoto_container.date
        if album_date:
            # Adjust to just a date
            album_date = datetime.datetime(album_date.year, album_date.month,
                                           album_date.day)
            timestamp = get_picasaweb_date(album_date)
        else:
            timestamp = None
        self.online_album = online_albums.get(self.name)
        if not self.online_album:
            print "Creating album: " + su.fsenc(self.name)
            if not options.dryrun:
                client.throttle()
                self.online_album = client.gd_client.InsertAlbum(
                    title=self.name,
                    summary=comments,
                    access='private',
                    timestamp=timestamp)
                online_albums[self.name] = self.online_album
            return

        # Check the properties of the online album
        changed = False
        online_album_summary_text = su.unicode_string(
            self.online_album.summary.text)
        if online_album_summary_text != comments:
            print 'Updating summary for online album %s (%s vs. %s)' % (
                su.fsenc(self.name), su.fsenc(online_album_summary_text),
                su.fsenc(comments))
            self.online_album.summary.text = comments
            changed = True

        if (timestamp and timestamp != self.online_album.timestamp.text):
            print 'Updating timestamp for online album %s (%s/%s)' % (su.fsenc(
                self.name), self.online_album.timestamp.datetime(), album_date)
            self.online_album.timestamp.text = timestamp
            changed = True

        if changed and not options.dryrun:
            client.throttle()
            try:
                self.online_album = client.gd_client.Put(
                    self.online_album,
                    self.online_album.GetEditLink().href,
                    converter=gdata.photos.AlbumEntryFromString)
            except gdata.photos.service.GooglePhotosException, e:
                print 'Failed to update data for online album %s: %s' % (
                    self.name, str(e))
Esempio n. 11
0
    def confirm(self, path, message, choices):  #IGNORE:R0911
        '''Prompts for confirmation.
        
        An item in the approve list always returns 1.
        An item in the reject list always returns 0.
        An empty response (hitting just enter) always returns 0.
        A response of +... adds a pattern to the approve list and returns 1.
        A response of -... adds a pattern to the reject list and returns 0.
        A response startring with y returns 1.
        The first character of any other response is matched against the letters
        in the choices parameters. If a match is found, the position is returned.
        For example, if choices is "nyc", entering c... returns 2.
        All other input returns 0.
        All matching is done without case sensitivity, and choices should be all
        lower case.
        
        @param theFile a <code>File</code> value
        @param message a <code>String</code> value
        @param choices a <code>String</code> value
        @return an <code>int</code> value
        '''
        for pattern in self.approve_list:
            if path.find(pattern) != -1:
                return 1

        for pattern in self.reject_list:
            if path.find(pattern) != -1:
                return 0

        answer = raw_input(su.fsenc(message))
        if len(answer) == 0:
            return 0
        first_char = answer[0].lower()
        if len(answer) > 1 and first_char == '+':
            self.approve_list.append(answer[1:])
            return 1

        if len(answer) > 1 and first_char == '-':
            self.reject_list.append(answer[1:])
            return 0

        if first_char == 'y':
            return 1

        for c in range(0, len(choices)):
            if first_char == choices[c]:
                return c
        return 0
Esempio n. 12
0
    def confirm(self, path, message, choices):  # IGNORE:R0911
        """Prompts for confirmation.
        
        An item in the approve list always returns 1.
        An item in the reject list always returns 0.
        An empty response (hitting just enter) always returns 0.
        A response of +... adds a pattern to the approve list and returns 1.
        A response of -... adds a pattern to the reject list and returns 0.
        A response startring with y returns 1.
        The first character of any other response is matched against the letters
        in the choices parameters. If a match is found, the position is returned.
        For example, if choices is "nyc", entering c... returns 2.
        All other input returns 0.
        All matching is done without case sensitivity, and choices should be all
        lower case.
        
        @param theFile a <code>File</code> value
        @param message a <code>String</code> value
        @param choices a <code>String</code> value
        @return an <code>int</code> value
        """
        for pattern in self.approve_list:
            if path.find(pattern) != -1:
                return 1

        for pattern in self.reject_list:
            if path.find(pattern) != -1:
                return 0

        answer = raw_input(su.fsenc(message))
        if len(answer) == 0:
            return 0
        first_char = answer[0].lower()
        if len(answer) > 1 and first_char == "+":
            self.approve_list.append(answer[1:])
            return 1

        if len(answer) > 1 and first_char == "-":
            self.reject_list.append(answer[1:])
            return 0

        if first_char == "y":
            return 1

        for c in range(0, len(choices)):
            if first_char == choices[c]:
                return c
        return 0
Esempio n. 13
0
        def save(self):
            """Saves the current options into a file."""
            config = ConfigParser.RawConfigParser()
            s = 'Export1'
            config.add_section(s)
            config.set(s, 'iphoto', self.iphoto)
            config.set(s, 'export', self.export)
            config.set(s, 'albums', su.fsenc(self.albums))
            config.set(s, 'events', su.fsenc(self.events))
            config.set(s, 'smarts', su.fsenc(self.smarts))
            config.set(s, 'foldertemplate', su.fsenc(self.foldertemplate))
            config.set(s, 'nametemplate', su.fsenc(self.nametemplate))
            config.set(s, 'captiontemplate', su.fsenc(self.captiontemplate))
            config.set(s, 'max_create', self.max_create)
            config.set(s, 'delete', self.delete)
            config.set(s, 'max_delete', self.max_delete)
            config.set(s, 'update', self.update)
            config.set(s, 'max_udpate', self.max_update)
            config.set(s, 'link', self.link)
            config.set(s, 'dryrun', self.dryrun)
            config.set(s, 'folderhints', self.folderhints)
            config.set(s, 'captiontemplate', self.captiontemplate)
            config.set(s, 'nametemplate', self.nametemplate)
            config.set(s, 'reverse', self.reverse)
            config.set(s, 'size', self.size)
            config.set(s, 'picasa', self.picasa)
            config.set(s, 'movies', self.movies)
            config.set(s, 'originals', self.originals)
            config.set(s, 'iptc', self.iptc)
            config.set(s, 'gps', self.gps)
            config.set(s, 'faces', self.faces)
            config.set(s, 'facealbums', self.facealbums)
            config.set(s, 'facealbum_prefix', self.facealbum_prefix)
            config.set(s, 'face_keywords', self.face_keywords)

            config_folder = os.path.split(_CONFIG_PATH)[0]
            if not os.path.exists(config_folder):
                os.makedirs(config_folder)
            configfile = open(_CONFIG_PATH, 'wb')
            config.write(configfile)
            configfile.close()
        def save(self):
            """Saves the current options into a file."""
            config = ConfigParser.RawConfigParser()
            s = 'Export1'
            config.add_section(s)
            config.set(s, 'iphoto', self.iphoto)
            config.set(s, 'export', self.export)
            config.set(s, 'albums', su.fsenc(self.albums))
            config.set(s, 'events', su.fsenc(self.events))
            config.set(s, 'smarts', su.fsenc(self.smarts))
            config.set(s, 'foldertemplate', su.fsenc(self.foldertemplate))
            config.set(s, 'nametemplate', su.fsenc(self.nametemplate))
            config.set(s, 'captiontemplate', su.fsenc(self.captiontemplate))
            config.set(s, 'max_create', self.max_create)
            config.set(s, 'delete', self.delete)
            config.set(s, 'max_delete', self.max_delete)
            config.set(s, 'update', self.update)
            config.set(s, 'max_udpate', self.max_update)
            config.set(s, 'link', self.link)
            config.set(s, 'dryrun', self.dryrun)
            config.set(s, 'folderhints', self.folderhints)
            config.set(s, 'captiontemplate', self.captiontemplate)
            config.set(s, 'nametemplate', self.nametemplate)
            config.set(s, 'reverse', self.reverse)
            config.set(s, 'size', self.size)
            config.set(s, 'picasa', self.picasa)
            config.set(s, 'movies', self.movies)
            config.set(s, 'originals', self.originals)
            config.set(s, 'iptc', self.iptc)
            config.set(s, 'gps', self.gps)
            config.set(s, 'faces', self.faces)
            config.set(s, 'facealbums', self.facealbums)
            config.set(s, 'facealbum_prefix', self.facealbum_prefix)
            config.set(s, 'face_keywords', self.face_keywords)

            config_folder = os.path.split(_CONFIG_PATH)[0]
            if not os.path.exists(config_folder):
                os.makedirs(config_folder)
            configfile = open(_CONFIG_PATH, 'wb')
            config.write(configfile)
            configfile.close()
Esempio n. 15
0
    def generate_update(self, client, options):
        """Attempts to update a photo. If the media file needs updating, deletes
        it first, then adds it back in.

        Args:
           client - the PicasaWeb client
           album_id - the id of the album for this photo
        """
        # check albumFile
        self.picasa_photo = check_media_update(client, self.picasa_photo,
                                               self.photo, self.export_file,
                                               options)
        picasa_photo = self.picasa_photo

        # Now check if any of the meta data needs to be updated.
        needs_update = False
        picasa_title = su.unicode_string(picasa_photo.title.text)
        if self.title != picasa_title:
            print(
                'Updating meta data for %s because it has Caption "%s" '
                'instead of "%s".') % (su.fsenc(
                    self.export_file), su.fsenc(picasa_title),
                                       su.fsenc(self.title))
            picasa_photo.title.text = self.title
            needs_update = True

        # Combine title and description because PicasaWeb does not show the
        # title anywhere.
        comment = imageutils.get_photo_caption(self.photo,
                                               options.captiontemplate)
        online_summary = su.unicode_string(picasa_photo.summary.text)
        if not su.equalscontent(comment, online_summary):
            print("Updating meta data for " + su.fsenc(self.export_file) +
                  ' because it has description "' + su.fsenc(online_summary) +
                  '" instead of "' + su.fsenc(comment) + '".')
            picasa_photo.summary.text = comment.strip()
            needs_update = True

        if self.photo.date:
            photo_time = get_picasaweb_date(self.photo.date)
            if photo_time != picasa_photo.timestamp.text:
                print(
                    'Updating meta data for %s because it has timestamp "'
                    '%s" instead of "%s"') % (su.fsenc(
                        self.export_file), picasa_photo.timestamp.datetime(),
                                              self.photo.date)
                picasa_photo.timestamp.text = photo_time
                needs_update = True

        export_keywords = self.get_export_keywords(options)
        picasa_keywords = []
        if (picasa_photo.media and picasa_photo.media.keywords
                and picasa_photo.media.keywords.text):
            picasa_keywords = su.unicode_string(
                picasa_photo.media.keywords.text).split(', ')
        else:
            picasa_keywords = []
        if not imageutils.compare_keywords(export_keywords, picasa_keywords):
            print("Updating meta data for " + su.fsenc(self.export_file) +
                  " because of keywords (" +
                  su.fsenc(",".join(picasa_keywords)) + ") instead of (" +
                  su.fsenc(",".join(export_keywords)) + ").")
            if not picasa_photo.media:
                picasa_photo.media = gdata.media.Group()
            if not picasa_photo.media.keywords:
                picasa_photo.media.keywords = gdata.media.Keywords()
            picasa_photo.media.keywords.text = ', '.join(export_keywords)
            needs_update = True

        if options.gps and self.photo.gps:
            if picasa_photo.geo and picasa_photo.geo.Point:
                picasa_location = imageutils.GpsLocation().from_gdata_point(
                    picasa_photo.geo.Point)
            else:
                picasa_location = imageutils.GpsLocation()
            if not picasa_location.is_same(self.photo.gps):
                print("Updating meta data for " + su.fsenc(self.export_file) +
                      " because of GPS " + picasa_location.to_string() +
                      " vs " + self.photo.gps.to_string())
                set_picasa_photo_pos(picasa_photo, self.photo.gps)
                needs_update = True

        if not needs_update:
            return

        if not options.update:
            print "Needs update: " + su.fsenc(self.export_file) + "."
            print "Use the -u option to update this file."
            return
        print("Updating metadata: " + self.export_file)
        if options.dryrun:
            return
        retry = 0
        wait_time = 1.0
        while True:
            try:
                client.throttle()
                picasa_photo = client.gd_client.UpdatePhotoMetadata(
                    picasa_photo)
                return
            except gdata.photos.service.GooglePhotosException, e:
                retry += 1
                if retry == 10:
                    raise e
                if str(e).find("17 REJECTED_USER_LIMIT") == -1:
                    raise e
                wait_time = wait_time * 2
                print("Retrying after " + wait_time + "s because of " + str(e))
                time.sleep(wait_time)
Esempio n. 16
0
    def generate_update(self, client, options):
        """Attempts to update a photo. If the media file needs updating, deletes
        it first, then adds it back in.

        Args:
           client - the PicasaWeb client
           album_id - the id of the album for this photo
        """
        # check albumFile
        self.picasa_photo = check_media_update(client, self.picasa_photo, self.photo, self.export_file, options)
        picasa_photo = self.picasa_photo

        # Now check if any of the meta data needs to be updated.
        needs_update = False
        picasa_title = su.unicode_string(picasa_photo.title.text)
        if self.title != picasa_title:
            print ('Updating meta data for %s because it has Caption "%s" ' 'instead of "%s".') % (
                su.fsenc(self.export_file),
                su.fsenc(picasa_title),
                su.fsenc(self.title),
            )
            picasa_photo.title.text = self.title
            needs_update = True

        # Combine title and description because PicasaWeb does not show the
        # title anywhere.
        comment = imageutils.get_photo_caption(self.photo, options.captiontemplate)
        online_summary = su.unicode_string(picasa_photo.summary.text)
        if not su.equalscontent(comment, online_summary):
            print (
                "Updating meta data for "
                + su.fsenc(self.export_file)
                + ' because it has description "'
                + su.fsenc(online_summary)
                + '" instead of "'
                + su.fsenc(comment)
                + '".'
            )
            picasa_photo.summary.text = comment.strip()
            needs_update = True

        if self.photo.date:
            photo_time = get_picasaweb_date(self.photo.date)
            if photo_time != picasa_photo.timestamp.text:
                print ('Updating meta data for %s because it has timestamp "' '%s" instead of "%s"') % (
                    su.fsenc(self.export_file),
                    picasa_photo.timestamp.datetime(),
                    self.photo.date,
                )
                picasa_photo.timestamp.text = photo_time
                needs_update = True

        export_keywords = self.get_export_keywords(options)
        picasa_keywords = []
        if picasa_photo.media and picasa_photo.media.keywords and picasa_photo.media.keywords.text:
            picasa_keywords = su.unicode_string(picasa_photo.media.keywords.text).split(", ")
        else:
            picasa_keywords = []
        if not imageutils.compare_keywords(export_keywords, picasa_keywords):
            print (
                "Updating meta data for "
                + su.fsenc(self.export_file)
                + " because of keywords ("
                + su.fsenc(",".join(picasa_keywords))
                + ") instead of ("
                + su.fsenc(",".join(export_keywords))
                + ")."
            )
            if not picasa_photo.media:
                picasa_photo.media = gdata.media.Group()
            if not picasa_photo.media.keywords:
                picasa_photo.media.keywords = gdata.media.Keywords()
            picasa_photo.media.keywords.text = ", ".join(export_keywords)
            needs_update = True

        if options.gps and self.photo.gps:
            if picasa_photo.geo and picasa_photo.geo.Point:
                picasa_location = imageutils.GpsLocation().from_gdata_point(picasa_photo.geo.Point)
            else:
                picasa_location = imageutils.GpsLocation()
            if not picasa_location.is_same(self.photo.gps):
                print (
                    "Updating meta data for "
                    + su.fsenc(self.export_file)
                    + " because of GPS "
                    + picasa_location.to_string()
                    + " vs "
                    + self.photo.gps.to_string()
                )
                set_picasa_photo_pos(picasa_photo, self.photo.gps)
                needs_update = True

        if not needs_update:
            return

        if not options.update:
            print "Needs update: " + su.fsenc(self.export_file) + "."
            print "Use the -u option to update this file."
            return
        print ("Updating metadata: " + self.export_file)
        if options.dryrun:
            return
        retry = 0
        wait_time = 1.0
        while True:
            try:
                client.throttle()
                picasa_photo = client.gd_client.UpdatePhotoMetadata(picasa_photo)
                return
            except gdata.photos.service.GooglePhotosException, e:
                retry += 1
                if retry == 10:
                    raise e
                if str(e).find("17 REJECTED_USER_LIMIT") == -1:
                    raise e
                wait_time = wait_time * 2
                print ("Retrying after " + wait_time + "s because of " + str(e))
                time.sleep(wait_time)