├── .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 |
--------------------------------------------------------------------------------