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(id, token
, filename
):
158 # Contruct a request to find the sizes
159 url
= "http://api.flickr.com/services/rest/?method=flickr.photos.getSizes"
160 url
+= "&photo_id=" + id
163 url
= flickrsign(url
, token
)
166 response
= urllib2
.urlopen(url
)
169 dom
= xml
.dom
.minidom
.parse(response
)
171 # Get the list of sizes
172 sizes
= dom
.getElementsByTagName("size")
174 # Grab the original if it exists
175 if (sizes
[-1].getAttribute("label") == "Original"):
176 imgurl
= sizes
[-1].getAttribute("source")
178 print "Failed to get original for photo id " + id
181 # Free the DOM memory
184 # Grab the image file
185 response
= urllib2
.urlopen(imgurl
)
186 data
= response
.read()
189 fh
= open(filename
, "w")
195 print "Failed to retrieve photo id " + id
197 ######## Main Application ##########
198 if __name__
== '__main__':
200 # The first, and only argument needs to be a directory
202 os
.chdir(sys
.argv
[1])
204 print "usage: %s directory" % sys
.argv
[0]
207 # First things first, see if we have a cached user and auth-token
209 cache
= open("touchr.frob.cache", "r")
210 config
= cPickle
.load(cache
)
213 # We don't - get a new one
215 (user
, token
) = froblogin(getfrob(), "read")
216 config
= { "version":1 , "user":user, "token":token }
218 # Save it for future use
219 cache
= open("touchr.frob.cache", "w")
220 cPickle
.dump(config
, cache
)
223 # Now, construct a query for the list of photo sets
224 url
= "http://api.flickr.com/services/rest/?method=flickr.photosets.getList"
225 url
+= "&user_id=" + config
["user"]
226 url
= flickrsign(url
, config
["token"])
229 response
= urllib2
.urlopen(url
)
232 dom
= xml
.dom
.minidom
.parse(response
)
234 # Get the list of Sets
235 sets
= dom
.getElementsByTagName("photoset")
237 # For each set - create a url
240 pid
= set.getAttribute("id")
241 dir = getText(set.getElementsByTagName("title")[0].childNodes
)
242 dir = unicodedata
.normalize('NFKD', dir.decode("utf-8", "ignore")).encode('ASCII', 'ignore') # Normalize to ASCII
244 # Build the list of photos
245 url
= "http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos"
246 url
+= "&photoset_id=" + pid
248 # Append to our list of urls
249 urls
.append( (url
, dir) )
251 # Free the DOM memory
254 # Add the photos which are not in any set
255 url
= "http://api.flickr.com/services/rest/?method=flickr.photos.getNotInSet"
256 urls
.append( (url
, "No Set") )
258 # Add the user's Favourites
259 url
= "http://api.flickr.com/services/rest/?method=flickr.favorites.getList"
260 urls
.append( (url
, "Favourites") )
262 # Time to get the photos
264 for (url
, dir) in urls
:
265 # Create the directory
271 # Get 500 results per page
272 url
+= "&per_page=500"
276 request
= url
+ "&page=" + str(page
)
279 request
= flickrsign(request
, config
["token"])
282 response
= urllib2
.urlopen(request
)
285 dom
= xml
.dom
.minidom
.parse(response
)
288 pages
= int(dom
.getElementsByTagName("photo")[0].parentNode
.getAttribute("pages"))
291 for photo
in dom
.getElementsByTagName("photo"):
292 # Tell the user we're grabbing the file
293 print photo
.getAttribute("title").encode("utf8") + " ... in set ... " + dir
296 photoid
= photo
.getAttribute("id")
299 target
= dir + "/" + photoid
+ ".jpg"
301 # Skip files that exist
302 if os
.access(target
, os
.R_OK
):
305 # Look it up in our dictionary of inodes first
306 if inodes
.has_key(photoid
) and os
.access(inodes
[photoid
], os
.R_OK
):
307 # woo, we have it already, use a hard-link
308 os
.link(inodes
[photoid
], target
)
310 inodes
[photoid
] = getphoto(photo
.getAttribute("id"), config
["token"], target
)
312 # Move on the next page