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)
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)
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)
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) )
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
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)
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
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))
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))
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))
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
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
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 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)
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)