├── .gitignore ├── conf.py.sample ├── .project ├── .pydevproject ├── README └── flickasa.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.project 2 | /.pydevproject 3 | /conf.py 4 | /picasa_videos 5 | -------------------------------------------------------------------------------- /conf.py.sample: -------------------------------------------------------------------------------- 1 | #SAVE THIS FILE AS conf.py and enter your credentials in the configuration variables below. 2 | 3 | #CONFIGURE YOUR GMAIL ACCOUNT CREDENTIALS 4 | GMAIL_ACCOUNT='yourEmail@gmail.com' 5 | GMAIL_PASSWORD='' 6 | 7 | #CONFIGURE YOUR FLICKR API CREDENTIALS 8 | #If you don't have them get them at 9 | #http://www.flickr.com/services/apps/create/noncommercial/ 10 | FLICKR_API_KEY='' 11 | FLICKR_API_SECRET='' -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | flickasa 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | 15 | org.python.pydev.pythonNature 16 | 17 | 18 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Default 6 | python 2.6 7 | 8 | /Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/flickrapi-1.4.2-py2.6.egg 9 | /Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/gdata-2.0.14-py2.6.egg 10 | /Library/Frameworks/Python.framework/Versions/2.6/lib/python2.6/site-packages/threadpool-1.2.7-py2.6.egg 11 | 12 | 13 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | -= INTRODUCTION =- 2 | 3 | With over 23,000 pictures on my Flickr account and Yahoo! closing projects left and right I began to feel a little nervous and I tried several options to export my Flickr pictures. 4 | Tried different free software solutions but none of them worked all the way, they would always fail in one way or another. Also, having all my pictures in my local drive isn't too convenient when it comes to sharing with friends and family. 5 | 6 | Google+ Circles launches with Picasa integration, a far more interesting combination to store my pictures than Facebook's limited main stream solution, now I knew I needed a script to migrate my 23,589 pictures from Flickr to Picasa. 7 | 8 | A friend on Google+ recommended a python script shown on a blog post at http://www.edparsons.com/2011/06/migrating-from-flickr-to-picasaweb/ 9 | 10 | This is slightly modified version of that script. It handles some exceptions that were probably not ocurring at the time the original author wrote it. As of July 2011 it managed to export all my 23,589 pictures (without breaking to an Exception during the entire run) 11 | 12 | -= REQUIREMENTS =- 13 | 14 | Python 2.6 recommended. 15 | 16 | Requires flickrapi and gdata. You can install these by issuing the following commands on your terminal. 17 | 18 | $ easy_install-2.6 gdata 19 | $ easy_install-2.6 flickrapi 20 | $ easy_install-2.6 threadpool 21 | 22 | You will need a Flickr API Key and Secret to get the script to work. 23 | You can obtain these at http://www.flickr.com/services/apps/create/noncommercial/ 24 | Note: if "easy_install-2.6" doesn't work Josiah Ritchie says that "easy-install" worked for him in Ubuntu. 25 | 26 | -= CONFIGURATION =- 27 | 28 | Save 'conf.py.sample' as 'conf.py' and enter the appropiate Flickr and Picasa credentials in the configuration variables declared in it. 29 | 30 | -= RUNNING =- 31 | 32 | After you've configured your credentials just fire up the script on your terminal 33 | 34 | $ python flickasa.py 35 | 36 | If for some weird reason the script stops, just launch it again, it will re-scan your flickr account again but it will pickup right where it left the last time. 37 | 38 | -= LICENSE =- 39 | 40 | Do whatever you want with it, the original script didn't come with a license, so I'm not attaching a license to it either. -------------------------------------------------------------------------------- /flickasa.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/python 2 | # 3 | # requires flickrapi and gdata 4 | # 5 | # It's a little ugly, but it is heavily tested and works! 6 | import flickrapi, StringIO 7 | import gdata 8 | import gdata.data 9 | import gdata.photos.service 10 | from getpass import getpass 11 | from urllib import urlretrieve 12 | from tempfile import mkstemp 13 | from threadpool import ThreadPool, WorkRequest 14 | import os 15 | import sys, os.path, StringIO 16 | import time 17 | import random 18 | import gdata.service 19 | import gdata 20 | import atom.service 21 | import atom 22 | import gdata.photos 23 | from shutil import copyfile 24 | 25 | from conf import * 26 | 27 | from gdata.photos.service import GPHOTOS_INVALID_ARGUMENT, GPHOTOS_INVALID_CONTENT_TYPE, GooglePhotosException 28 | 29 | video_too_large_save_location = os.path.join(os.path.sep.join(__file__.split(os.path.sep)[:-1]), 'picasa_videos') 30 | 31 | if not os.path.exists(video_too_large_save_location): 32 | os.mkdir(video_too_large_save_location) 33 | 34 | class VideoEntry(gdata.photos.PhotoEntry): 35 | pass 36 | 37 | gdata.photos.VideoEntry = VideoEntry 38 | 39 | def InsertVideo(self, album_or_uri, video, filename_or_handle, content_type='image/jpeg'): 40 | """Copy of InsertPhoto which removes protections since it *should* work""" 41 | try: 42 | assert(isinstance(video, VideoEntry)) 43 | except AssertionError: 44 | raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, 45 | 'body':'`video` must be a gdata.photos.VideoEntry instance', 46 | 'reason':'Found %s, not PhotoEntry' % type(video) 47 | }) 48 | try: 49 | majtype, mintype = content_type.split('/') 50 | #assert(mintype in SUPPORTED_UPLOAD_TYPES) 51 | except (ValueError, AssertionError): 52 | raise GooglePhotosException({'status':GPHOTOS_INVALID_CONTENT_TYPE, 53 | 'body':'This is not a valid content type: %s' % content_type, 54 | 'reason':'Accepted content types:' 55 | }) 56 | if isinstance(filename_or_handle, (str, unicode)) and \ 57 | os.path.exists(filename_or_handle): # it's a file name 58 | mediasource = gdata.MediaSource() 59 | mediasource.setFile(filename_or_handle, content_type) 60 | elif hasattr(filename_or_handle, 'read'):# it's a file-like resource 61 | if hasattr(filename_or_handle, 'seek'): 62 | filename_or_handle.seek(0) # rewind pointer to the start of the file 63 | # gdata.MediaSource needs the content length, so read the whole image 64 | file_handle = StringIO.StringIO(filename_or_handle.read()) 65 | name = 'image' 66 | if hasattr(filename_or_handle, 'name'): 67 | name = filename_or_handle.name 68 | mediasource = gdata.MediaSource(file_handle, content_type, 69 | content_length=file_handle.len, file_name=name) 70 | else: #filename_or_handle is not valid 71 | raise GooglePhotosException({'status':GPHOTOS_INVALID_ARGUMENT, 72 | 'body':'`filename_or_handle` must be a path name or a file-like object', 73 | 'reason':'Found %s, not path name or object with a .read() method' % \ 74 | type(filename_or_handle) 75 | }) 76 | 77 | if isinstance(album_or_uri, (str, unicode)): # it's a uri 78 | feed_uri = album_or_uri 79 | elif hasattr(album_or_uri, 'GetFeedLink'): # it's a AlbumFeed object 80 | feed_uri = album_or_uri.GetFeedLink().href 81 | 82 | try: 83 | return self.Post(video, uri=feed_uri, media_source=mediasource, 84 | converter=None) 85 | except gdata.service.RequestError, e: 86 | raise GooglePhotosException(e.args[0]) 87 | 88 | gdata.photos.service.PhotosService.InsertVideo = InsertVideo 89 | 90 | def clear_input_retriever(setting): 91 | return raw_input(setting.name + ":") 92 | 93 | def passwd_input_retriever(setting): 94 | return getpass(setting.name + ':') 95 | 96 | class Setting(object): 97 | 98 | def __init__(self, name, default=None, input_retriever=clear_input_retriever, empty_value=None): 99 | self.name = name 100 | self._value = default 101 | self.input_retriever = input_retriever 102 | self.empty_value = empty_value 103 | 104 | @property 105 | def value(self): 106 | while self._value == self.empty_value: 107 | self._value = self.input_retriever(self) 108 | 109 | return self._value 110 | 111 | 112 | FLICKR = None 113 | 114 | picasa_username = Setting('Picasa Username(complete email)',GMAIL_ACCOUNT) 115 | picasa_password = Setting('Picasa Password', default=GMAIL_PASSWORD,input_retriever=passwd_input_retriever) 116 | 117 | flickr_api_key = Setting('Flickr API Key',default=FLICKR_API_KEY) 118 | flickr_api_secret = Setting('Flickr API Secret',default=FLICKR_API_SECRET) 119 | 120 | flickr_usernsid = None 121 | 122 | def flickr_token_retriever(setting): 123 | global FLICKR 124 | global flickr_usernsid 125 | if FLICKR is None: 126 | FLICKR = flickrapi.FlickrAPI(flickr_api_key.value, flickr_api_secret.value) 127 | 128 | (token, frob) = FLICKR.get_token_part_one(perms='write') 129 | 130 | if not token: raw_input("Press ENTER after you authorized this program") 131 | 132 | FLICKR.get_token_part_two((token, frob)) 133 | 134 | flickr_usernsid = FLICKR.auth_checkToken(auth_token=token).find('auth').find('user').get('nsid') 135 | 136 | return True 137 | 138 | 139 | def get_gd_client(): 140 | 141 | gd_client = gdata.photos.service.PhotosService() 142 | gd_client.email = picasa_username.value 143 | gd_client.password = picasa_password.value 144 | gd_client.source = 'migrate-flickr-to-picasa.py' 145 | gd_client.ProgrammaticLogin() 146 | 147 | return gd_client 148 | 149 | def do_migration(threadpoolsize=7): 150 | 151 | print 'Authenticating with Picasa...' 152 | gd_client = get_gd_client() 153 | 154 | print 'Authenticating with Flickr..' 155 | flickr_token = Setting('Flickr Token', input_retriever=flickr_token_retriever) 156 | token = flickr_token.value # force retrieval of authentication information... 157 | 158 | sets = FLICKR.photosets_getList().find('photosets').getchildren() 159 | 160 | print 'Found %i sets to move over to Picasa.' % len(sets) 161 | 162 | 163 | def get_picasa_albums(id, aset, num_photos): 164 | all_picasa_albums = gd_client.GetUserFeed(user=picasa_username.value).entry 165 | picasa_albums = [] 166 | id = id.strip() 167 | 168 | orig_id = id 169 | 170 | for i in range((num_photos/1000) + 1): 171 | if i > 0: 172 | id = orig_id + '-' + str(i) 173 | 174 | picasa_album = None 175 | 176 | for album in all_picasa_albums: 177 | if album.title.text == id: 178 | picasa_album = album 179 | break 180 | 181 | if picasa_album is not None: 182 | print '"%s" set already exists as an album in Picasa.' % id 183 | else: 184 | description = aset.find('description').text 185 | if description is not None and len(description) > 1000: 186 | description = description[:1000] 187 | picasa_album = gd_client.InsertAlbum(title=id, summary=description, access='protected') 188 | print 'Created picasa album "%s".' % picasa_album.title.text 189 | 190 | picasa_albums.append(picasa_album) 191 | 192 | return picasa_albums 193 | 194 | 195 | def get_picasa_photos(picasa_albums): 196 | photos = [] 197 | 198 | for album in picasa_albums: 199 | photos.extend(gd_client.GetFeed(album.GetFeedLink().href).entry) 200 | 201 | return photos 202 | 203 | def get_photo_url(photo): 204 | if photo.get('media') == 'video': 205 | return "http://www.flickr.com/photos/%s/%s/play/orig/%s" % (flickr_usernsid, photo.get('id'), photo.get('originalsecret')) 206 | else: 207 | return photo.get('url_o') 208 | 209 | 210 | def move_photo(flickr_photo, picasa_album): 211 | 212 | def download_callback(count, blocksize, totalsize): 213 | 214 | download_stat_print = set((0.0, .25, .5, 1.0)) 215 | downloaded = float(count*blocksize) 216 | res = int((downloaded/totalsize)*100.0) 217 | 218 | for st in download_stat_print: 219 | dl = totalsize*st 220 | diff = downloaded - dl 221 | if diff >= -(blocksize/2) and diff <= (blocksize/2): 222 | downloaded_so_far = float(count*blocksize)/1024.0/1024.0 223 | total_size_in_mb = float(totalsize)/1024.0/1024.0 224 | print "photo: %s, album: %s --- %i%% - %.1f/%.1fmb" % (flickr_photo.get('title'), picasa_album.title.text, res, downloaded_so_far, total_size_in_mb) 225 | 226 | dest = os.path.join(video_too_large_save_location, flickr_photo.get('title')) 227 | if os.path.exists(dest): 228 | print 'Video "%s" of "%s" already exists in download cache of files over 100MB. Aborting download.' % (flickr_photo.get('title'), picasa_album.title.text) 229 | return 230 | 231 | photo_url = get_photo_url(flickr_photo) 232 | if photo_url is None: 233 | print 'Could not get photo url, skipping.' 234 | return 235 | 236 | 237 | print 'Downloading photo "%s" at url "%s".' % (flickr_photo.get('title'), photo_url) 238 | (fd, filename) = tmp_file = mkstemp() 239 | (filename, headers) = urlretrieve(photo_url, filename, download_callback) 240 | print 'Download Finished of %s for album %s at %s.' % (flickr_photo.get('title'), picasa_album.title.text, photo_url) 241 | 242 | size = os.stat(filename)[6] 243 | if size >= 100*1024*1024: 244 | print 'File "%s" of set "%s" larger than 100mb. Moving to download directory for manual handling. ' % (flickr_photo.get('title'), picasa_album.title.text) 245 | copyfile(filename, dest) 246 | os.close(fd) 247 | os.remove(filename) 248 | return 249 | 250 | print 'Uploading photo %s of album %s to Picasa.' % (flickr_photo.get('title'), picasa_album.title.text) 251 | 252 | if flickr_photo.get('media') == 'photo': 253 | picasa_photo = gdata.photos.PhotoEntry() 254 | else: 255 | picasa_photo = VideoEntry() 256 | 257 | picasa_photo.title = atom.Title(text=flickr_photo.get('title')) 258 | picasa_photo.summary = atom.Summary(text=flickr_photo.get('description'), summary_type='text') 259 | photo_info = FLICKR.photos_getInfo(photo_id=flickr_photo.get('id')).find('photo') 260 | picasa_photo.media.keywords = gdata.media.Keywords() 261 | picasa_photo.media.keywords.text = ', '.join([t.get('raw') for t in photo_info.find('tags').getchildren()]) 262 | picasa_photo.summary.text = photo_info.find('description').text or flickr_photo.get('title') 263 | 264 | #random pause between 1 to 5 seconds 265 | s = random.randint(0,3) 266 | if s > 0: 267 | print 'Sleeping ' + str(s) + ' seconds...' 268 | time.sleep(s) 269 | 270 | 271 | if flickr_photo.get('media') == 'photo': 272 | try: 273 | gd_client.InsertPhoto(picasa_album, picasa_photo, filename, content_type=headers.get('content-type', 'image/jpeg')) 274 | except GooglePhotosException,e: 275 | os.close(fd) 276 | os.remove(filename) 277 | return 278 | else: 279 | gd_client.InsertVideo(picasa_album, picasa_photo, filename, content_type=headers.get('content-type', 'video/avi')) 280 | 281 | print 'Upload Finished of %s for album %s.' % (flickr_photo.get('title'), picasa_album.title.text) 282 | 283 | os.close(fd) 284 | os.remove(filename) 285 | 286 | 287 | threadpool = ThreadPool(threadpoolsize) 288 | 289 | for aset_id in range(len(sets)): # go through each flickr set 290 | aset = sets[aset_id] 291 | set_title = aset.find('title').text 292 | print 'Moving "%s" set over to a picasa album. %i/%i' % (set_title, aset_id + 1, len(sets)) 293 | 294 | print 'Gathering set "%s" information.' % set_title 295 | 296 | num_photos = int(aset.get('photos')) + int(aset.get('videos')) 297 | all_photos = [] 298 | 299 | page = 1 300 | while len(all_photos) < num_photos: 301 | all_photos.extend( 302 | FLICKR.photosets_getPhotos( 303 | photoset_id=aset.get('id'), 304 | per_page=500, 305 | extras="url_o,media,original_format", 306 | page=page, 307 | media='all' 308 | ).find('photoset').getchildren() 309 | ) 310 | page += 1 311 | 312 | 313 | print 'Found %i photos and videos in the %s flickr set.' % (num_photos, set_title) 314 | 315 | picasa_albums = get_picasa_albums(set_title, aset, len(all_photos)) 316 | picasa_photos = get_picasa_photos(picasa_albums) 317 | 318 | for photo_id in range(len(all_photos)): 319 | 320 | photo = all_photos[photo_id] 321 | photo_found = False 322 | 323 | for p_photo in picasa_photos: 324 | if p_photo.title.text == photo.get('title'): 325 | print 'Already have photo "%s", skipping' % photo.get('title') 326 | photo_found = True 327 | break 328 | 329 | if photo_found: 330 | continue 331 | else: 332 | print 'Queuing photo %i/%i, %s of album %s for moving.' % (photo_id + 1, len(all_photos), photo.get('title'), set_title) 333 | 334 | p_album = None 335 | for album in picasa_albums: 336 | if int(album.numphotosremaining.text) > 0: 337 | album.numphotosremaining.text = str(int(album.numphotosremaining.text) - 1) 338 | p_album = album 339 | break 340 | 341 | req = WorkRequest(move_photo, [photo, p_album], {}) 342 | threadpool.putRequest(req) 343 | 344 | 345 | threadpool.wait() 346 | 347 | 348 | if __name__ == "__main__": 349 | 350 | print """ 351 | This script will move all the photos and sets from flickr over to picasa. 352 | That will require getting authentication information from both services... 353 | """ 354 | random.seed(time.time()) 355 | do_migration() 356 | --------------------------------------------------------------------------------