#!/usr/bin/python2 # Based on https://github.com/leocrawford/picasawebsync # "picasawebsync" has a very sophisticated mechanism for syncing a directory # structure on your disk with your Picasa-web photo collection. But it is # very awkward to use in cases which do not exactly correspond to this use # case. In particular, if you already have a large Picasa-web collection, # and now have a single directory you wish to add to it, this isn't easy to # do with picasawebsync, and has some unwelcome side effects (such as labeling # each and every picture with a visible label). # # This picasa-upload has one simple feature only: # picasa-upload album-name file1... # uploads a list of image files into an existing or new album. This allows # you to, for example, # 1. Upload the contents of a directory as a new album # 2. Upload some new files in a directory (which you know are # new) into an existing album. # # To simplify the user's life, uploading the same image again into the same # album does nothing (not uploading it a second time); This "sameness" is # judged by the md5 checksum of the file. Nevertheless, the current # implementation is not a full "sync" implementation: # 1. Files removed locally will not be removed in the web album. # 2. Files renamed locally will not be renamed (but will also not be uploaded # a second time, because the sameness is judged by the checksum, not the # file name.) # 3. Files modified locally will be uploaded (because the checksum changed) # but the old version of the file will not be deleted from the web album. # # Additional limitations/simplifications vs picasawebsync: # 1. This code doesn't support shrinking of images to the "free" # resolution. I view Picasaweb as a way to back up images, as # well as be able to retrieve them from any computer - and # keeping lower-resolution images on it is counter-productive. # 2. Uploading videos not supported yet. # # # To install: # * You'll need to set up your own OAuth 2.0 client ID, and # put it in ~/.client_secrets.json # * You'll need to install various python libraries with "pip # install" - gdata, oauth2client. See also instructions in # https://github.com/leocrawford/picasawebsync # # see also https://developers.google.com/picasa-web/docs/1.0/developers_guide_python import sys import os import hashlib import time import httplib2 import datetime import oauth2client import gdata.photos.service credentials = None def client(): global credentials if credentials is None: from oauth2client.file import Storage storage = Storage(os.path.expanduser("~/.picasa-upload")) credentials = storage.get() if credentials is None or credentials.invalid: print("Logging in to picasa web API.") flow = client.flow_from_clientsecrets( os.path.expanduser('~/.client_secrets.json'), scope='https://picasaweb.google.com/data/', redirect_uri='urn:ietf:wg:oauth:2.0:oob') auth_uri = flow.step1_get_authorize_url() print('Authorization URL: %s' % auth_uri) auth_code = raw_input('Enter the auth code: ') credentials = flow.step2_exchange(auth_code) storage.put(credentials) # The token we got usually expires after one hour, in which case we need # to "refresh" it (which does not involve the user with any manual steps). # This expiration can also happen in the middle of a very long upload # session, so we should call client() for each individual photo uploaded. # Note that instead of checking if the token is already expired # ("if credentials.access_token_expired"), we check if it only has 5 # minutes until it is about to expire - to avoid races. if (credentials.token_expiry - datetime.datetime.utcnow() < datetime.timedelta(minutes=5)): print("Refreshing expired login token.") credentials.refresh(httplib2.Http()) return gdata.photos.service.PhotosService(email='default', additional_headers={'Authorization' : 'Bearer %s' % credentials.access_token}) # Create a new private album with the given title and today's date. # It doesn't matter if an album with the same title already exists - a new # unique album will be created anyway. # The new album is created "protected", meaning only visible for its owner. # Counter-intuitively, had we used "private" that would have meant "visible # to everyone with the link". Protected is the safest default, and the user # should manually change this visiblity, and/or share the album, through the # Web interface. # This function returns an object for the new album - which can be used, # for example, to add photos to this album. def new_album(title): print("Creating new album %s" % title) # we could also set "timestamp" to override the default (current date). # Sadly, it seems the "commenting_enabled='false'" option is ignored, and # the resulting album still allows viewers to comment on the photos for # eternity (why would anyone want this feature???) return client().InsertAlbum(title=title, access='protected', summary='', commenting_enabled='false') # Calculate a photo file's checksum, so we can avoid uploading the same photo # again if it is already in the album. The specific algorithm used for the # checksum is not important, as the checksums are opaque cookies: We store # the checksum as metadata for each photo, and can later retrieve it and # compared to the checksum we calculate for the local file. def calculate_checksum(path): md5 = hashlib.md5() with open(path, 'rb') as f: for chunk in iter(lambda: f.read(128 * md5.block_size), b''): md5.update(chunk) return md5.hexdigest() arg_album = sys.argv[1] arg_files = sys.argv[2:] # Walk the picasa-web albums to find if are any matching the given title; # More than one album may match, as titles are not unique. If there are any # matches, give the user an option to choose an existing album and add photos # to it, instead of creating a new album. print("Looking for existing album with title '%s'" % arg_album) matching_albums = [] for webAlbum in client().GetUserFeed().entry: title = webAlbum.title.text if title == arg_album: matching_albums.append(webAlbum) choice = "" if matching_albums: print("Found %d album%s with the title '%s':" % (len(matching_albums), ("s" if len(matching_albums) > 1 else ""), arg_album)) i = 0 for album in matching_albums: i = i + 1 timestamp = album.timestamp.text date = time.strftime("%B %d %Y", time.localtime(int(timestamp)/1000)) nphotos = album.numphotos.text print(" %d) %s from %s, with %s photos" % (i, arg_album, date, nphotos)) print("Choose one of the albums above, or newline for new album.") while True: choice = raw_input("Choice (1-%d or newline): " % len(matching_albums)) try: if choice == "": break i = int(choice) if i >= 1 and i <= len(matching_albums): break except: pass checksums_in_album = set() if choice == "": web_album = new_album(arg_album) else: print("Using existing album") web_album = matching_albums[int(choice) - 1] # Fetch list of existing photos' checksums in this album, to avoid # uploading the same photo again. Note that this currently does not # "properly" handle renames (the old name remains) or photo modification # (the newly modified photo is uploaded, but the old version is not # deleted). for photo in client().GetFeed(web_album.GetPhotosUri()).entry: checksums_in_album.add(photo.checksum.text) # Upload all the given files to the album. for fn in arg_files: fn_checksum = calculate_checksum(fn) if fn_checksum in checksums_in_album: print("Skipping %s (already in album)" % fn) continue print("Uploading %s" % fn) metadata = gdata.photos.PhotoEntry() # The "title" is listed in picasaweb as the "Filename". # It does not need to be unique. metadata.title = gdata.atom.Title(text=os.path.basename(fn)) metadata.checksum = gdata.photos.Checksum(text=fn_checksum) photo = client().InsertPhoto(web_album, metadata, fn) # TODO: perhaps use date from photo.exif to consider modifying the # album's timestamp at the end?