4 # FlickrTouchr - a simple python script to grab all your photos from flickr,
5 # dump into a directory - organised into folders by set -
6 # along with any favourites you have saved.
8 # You can then sync the photos to an iPod touch.
12 # Original Author: colm - AT - allcosts.net - Colm MacCarthaigh - 2008-01-21
14 # Modified by: Dan Benjamin - http://hivelogic.com
16 # License: Apache 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html
19 import xml
.dom
.minidom
29 API_KEY
= "e224418b91b4af4e8cdb0564716fa9bd"
30 SHARED_SECRET
= "7cddb9c9716501a0"
33 # Utility functions for dealing with flickr authentication
35 def getText(nodelist
):
38 if node
.nodeType
== node
.TEXT_NODE
:
40 return rc
.encode("utf-8")
43 # Get the frob based on our API_KEY and shared secret
46 # Create our signing string
47 string
= SHARED_SECRET
+ "api_key" + API_KEY
+ "methodflickr.auth.getFrob"
48 hash = md5
.new(string
).digest().encode("hex")
50 # Formulate the request
51 url
= "http://api.flickr.com/services/rest/?method=flickr.auth.getFrob"
52 url
+= "&api_key=" + API_KEY
+ "&api_sig=" + hash
55 # Make the request and extract the frob
56 response
= urllib2
.urlopen(url
)
59 dom
= xml
.dom
.minidom
.parse(response
)
62 frob
= getText(dom
.getElementsByTagName("frob")[0].childNodes
)
71 raise "Could not retrieve frob"
74 # Login and get a token
76 def froblogin(frob
, perms
):
77 string
= SHARED_SECRET
+ "api_key" + API_KEY
+ "frob" + frob
+ "perms" + perms
78 hash = md5
.new(string
).digest().encode("hex")
80 # Formulate the request
81 url
= "http://api.flickr.com/services/auth/?"
82 url
+= "api_key=" + API_KEY
+ "&perms=" + perms
83 url
+= "&frob=" + frob
+ "&api_sig=" + hash
85 # Tell the user what's happening
86 print "In order to allow FlickrTouchr to read your photos and favourites"
87 print "you need to allow the application. Please press return when you've"
88 print "granted access at the following url (which should have opened"
89 print "automatically)."
93 print "Waiting for you to press return"
95 # We now have a login url, open it in a web-browser
96 webbrowser
.open_new(url
)
101 # Now, try and retrieve a token
102 string
= SHARED_SECRET
+ "api_key" + API_KEY
+ "frob" + frob
+ "methodflickr.auth.getToken"
103 hash = md5
.new(string
).digest().encode("hex")
105 # Formulate the request
106 url
= "http://api.flickr.com/services/rest/?method=flickr.auth.getToken"
107 url
+= "&api_key=" + API_KEY
+ "&frob=" + frob
108 url
+= "&api_sig=" + hash
110 # See if we get a token
112 # Make the request and extract the frob
113 response
= urllib2
.urlopen(url
)
116 dom
= xml
.dom
.minidom
.parse(response
)
118 # get the token and user-id
119 token
= getText(dom
.getElementsByTagName("token")[0].childNodes
)
120 nsid
= dom
.getElementsByTagName("user")[0].getAttribute("nsid")
125 # Return the token and userid
131 # Sign an arbitrary flickr request with a token
133 def flickrsign(url
, token
):
134 query
= urlparse
.urlparse(url
).query
135 query
+= "&api_key=" + API_KEY
+ "&auth_token=" + token
136 params
= query
.split('&')
138 # Create the string to hash
139 string
= SHARED_SECRET
141 # Sort the arguments alphabettically
144 string
+= param
.replace('=', '')
145 hash = md5
.new(string
).digest().encode("hex")
147 # Now, append the api_key, and the api_sig args
148 url
+= "&api_key=" + API_KEY
+ "&auth_token=" + token
+ "&api_sig=" + hash
150 # Return the signed url
154 # Grab the photo from the server
156 def getphoto(imgurl
, filename
):
157 # Grab the image file
158 response
= urllib2
.urlopen(imgurl
)
159 data
= response
.read()
162 fh
= open(filename
, "w")
169 # Escape Unicode chars
170 # http://stackoverflow.com/questions/3011569/how-do-i-convert-filenames-from-unicode-to-ascii
173 if isinstance(s
, str):
174 s
= s
.decode('utf-8')
181 return u
''.join(chars
)
183 ######## Main Application ##########
184 if __name__
== '__main__':
186 # The first, and only argument needs to be a directory
188 os
.chdir(sys
.argv
[1])
190 print "usage: %s directory" % sys
.argv
[0]
193 # First things first, see if we have a cached user and auth-token
195 cache
= open("touchr.frob.cache", "r")
196 config
= cPickle
.load(cache
)
199 # We don't - get a new one
201 (user
, token
) = froblogin(getfrob(), "read")
202 config
= { "version":1 , "user":user, "token":token }
204 # Save it for future use
205 cache
= open("touchr.frob.cache", "w")
206 cPickle
.dump(config
, cache
)
209 # Now, construct a query for the list of photo sets
210 url
= "http://api.flickr.com/services/rest/?method=flickr.photosets.getList"
211 url
+= "&user_id=" + config
["user"]
212 url
= flickrsign(url
, config
["token"])
215 response
= urllib2
.urlopen(url
)
218 dom
= xml
.dom
.minidom
.parse(response
)
220 # Get the list of Sets
221 sets
= dom
.getElementsByTagName("photoset")
223 # For each set - create a url
226 pid
= set.getAttribute("id")
227 dir = getText(set.getElementsByTagName("title")[0].childNodes
)
228 #dir = unicodedata.normalize('NFC', dir.decode("utf-8", "ignore")).encode('ASCII', 'ignore') # Normalize to ASCII
229 dir = unistrip(dir) # Normalize to ASCII, converting Unicode chars to '_'
231 # Build the list of photos
232 url
= "http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos"
233 url
+= "&photoset_id=" + pid
235 # Append to our list of urls
236 urls
.append( (url
, dir) )
238 # Free the DOM memory
241 # Add the photos which are not in any set
242 url
= "http://api.flickr.com/services/rest/?method=flickr.photos.getNotInSet"
243 urls
.append( (url
, "No Set") )
245 # Add the user's Favourites
246 url
= "http://api.flickr.com/services/rest/?method=flickr.favorites.getList"
247 urls
.append( (url
, "Favourites") )
249 # Time to get the photos
251 for (url
, dir) in urls
:
252 # Create the directory
258 # Get 500 results per page
259 url
+= "&per_page=500"
262 # Get Date-Taken and Original-size URL for each result photo
263 url
+= "&extras=date_taken,url_o,url_l,url_m"
266 request
= url
+ "&page=" + str(page
)
269 request
= flickrsign(request
, config
["token"])
272 response
= urllib2
.urlopen(request
)
275 dom
= xml
.dom
.minidom
.parse(response
)
278 pages
= int(dom
.getElementsByTagName("photo")[0].parentNode
.getAttribute("pages"))
281 for photo
in dom
.getElementsByTagName("photo"):
282 # Tell the user we're grabbing the file
283 print photo
.getAttribute("title").encode("utf8") + " ... in set ... " + dir
286 photoid
= photo
.getAttribute("id")
288 # Grab the taken date
289 taken
= photo
.getAttribute("datetaken")
290 taken
= taken
.replace(":","").replace("-","").replace(" ","")
292 # Get URL to the "Original" size of the photo,
293 # falling back to "Large" and then "Medium" if needed
294 imgurl
= photo
.getAttribute("url_o")
297 imgurl
= photo
.getAttribute("url_l")
300 imgurl
= photo
.getAttribute("url_m")
303 # Build the target filename
304 target
= dir + "/" + taken
+ "-" + photoid
+ imgsz
+ ".jpg"
306 # Skip files that exist
307 if os
.access(target
, os
.R_OK
):
308 inodes
[photoid
] = target
311 # Look it up in our dictionary of inodes first
312 if photoid
in inodes
and inodes
[photoid
] and os
.access(inodes
[photoid
], os
.R_OK
):
313 # woo, we have it already, use a hard-link
314 os
.link(inodes
[photoid
], target
)
316 # Grab image and save to local file
318 inodes
[photoid
] = getphoto(imgurl
, target
)
320 print "Failed to find URL for photo id " + photoid
322 # Move on the next page