]> Tony Duckles's Git Repositories (git.nynim.org) - flickrtouchr.git/blob - flickrtouchr.py
Fixing an issue with the way we look up the original size via Willie.
[flickrtouchr.git] / flickrtouchr.py
1 #!/usr/bin/env python
2
3 #
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.
7 #
8 # You can then sync the photos to an iPod touch.
9 #
10 # Version: 1.2
11 #
12 # Original Author: colm - AT - allcosts.net - Colm MacCarthaigh - 2008-01-21
13 #
14 # Modified by: Dan Benjamin - http://hivelogic.com
15 #
16 # License: Apache 2.0 - http://www.apache.org/licenses/LICENSE-2.0.html
17 #
18
19 import xml.dom.minidom
20 import webbrowser
21 import urlparse
22 import urllib2
23 import cPickle
24 import md5
25 import sys
26 import os
27
28 API_KEY = "e224418b91b4af4e8cdb0564716fa9bd"
29 SHARED_SECRET = "7cddb9c9716501a0"
30
31 #
32 # Utility functions for dealing with flickr authentication
33 #
34 def getText(nodelist):
35 rc = ""
36 for node in nodelist:
37 if node.nodeType == node.TEXT_NODE:
38 rc = rc + node.data
39 return rc.encode("utf-8")
40
41 #
42 # Get the frob based on our API_KEY and shared secret
43 #
44 def getfrob():
45 # Create our signing string
46 string = SHARED_SECRET + "api_key" + API_KEY + "methodflickr.auth.getFrob"
47 hash = md5.new(string).digest().encode("hex")
48
49 # Formulate the request
50 url = "http://api.flickr.com/services/rest/?method=flickr.auth.getFrob"
51 url += "&api_key=" + API_KEY + "&api_sig=" + hash
52
53 try:
54 # Make the request and extract the frob
55 response = urllib2.urlopen(url)
56
57 # Parse the XML
58 dom = xml.dom.minidom.parse(response)
59
60 # get the frob
61 frob = getText(dom.getElementsByTagName("frob")[0].childNodes)
62
63 # Free the DOM
64 dom.unlink()
65
66 # Return the frob
67 return frob
68
69 except:
70 raise "Could not retrieve frob"
71
72 #
73 # Login and get a token
74 #
75 def froblogin(frob, perms):
76 string = SHARED_SECRET + "api_key" + API_KEY + "frob" + frob + "perms" + perms
77 hash = md5.new(string).digest().encode("hex")
78
79 # Formulate the request
80 url = "http://api.flickr.com/services/auth/?"
81 url += "api_key=" + API_KEY + "&perms=" + perms
82 url += "&frob=" + frob + "&api_sig=" + hash
83
84 # Tell the user what's happening
85 print "In order to allow FlickrTouchr to read your photos and favourites"
86 print "you need to allow the application. Please press return when you've"
87 print "granted access at the following url (which should have opened"
88 print "automatically)."
89 print
90 print url
91 print
92 print "Waiting for you to press return"
93
94 # We now have a login url, open it in a web-browser
95 webbrowser.open_new(url)
96
97 # Wait for input
98 sys.stdin.readline()
99
100 # Now, try and retrieve a token
101 string = SHARED_SECRET + "api_key" + API_KEY + "frob" + frob + "methodflickr.auth.getToken"
102 hash = md5.new(string).digest().encode("hex")
103
104 # Formulate the request
105 url = "http://api.flickr.com/services/rest/?method=flickr.auth.getToken"
106 url += "&api_key=" + API_KEY + "&frob=" + frob
107 url += "&api_sig=" + hash
108
109 # See if we get a token
110 try:
111 # Make the request and extract the frob
112 response = urllib2.urlopen(url)
113
114 # Parse the XML
115 dom = xml.dom.minidom.parse(response)
116
117 # get the token and user-id
118 token = getText(dom.getElementsByTagName("token")[0].childNodes)
119 nsid = dom.getElementsByTagName("user")[0].getAttribute("nsid")
120
121 # Free the DOM
122 dom.unlink()
123
124 # Return the token and userid
125 return (nsid, token)
126 except:
127 raise "Login failed"
128
129 #
130 # Sign an arbitrary flickr request with a token
131 #
132 def flickrsign(url, token):
133 query = urlparse.urlparse(url).query
134 query += "&api_key=" + API_KEY + "&auth_token=" + token
135 params = query.split('&')
136
137 # Create the string to hash
138 string = SHARED_SECRET
139
140 # Sort the arguments alphabettically
141 params.sort()
142 for param in params:
143 string += param.replace('=', '')
144 hash = md5.new(string).digest().encode("hex")
145
146 # Now, append the api_key, and the api_sig args
147 url += "&api_key=" + API_KEY + "&auth_token=" + token + "&api_sig=" + hash
148
149 # Return the signed url
150 return url
151
152 #
153 # Grab the photo from the server
154 #
155 def getphoto(id, token, filename):
156 try:
157 # Contruct a request to find the sizes
158 url = "http://api.flickr.com/services/rest/?method=flickr.photos.getSizes"
159 url += "&photo_id=" + id
160
161 # Sign the request
162 url = flickrsign(url, token)
163
164 # Make the request
165 response = urllib2.urlopen(url)
166
167 # Parse the XML
168 dom = xml.dom.minidom.parse(response)
169
170 # Get the list of sizes
171 sizes = dom.getElementsByTagName("size")
172
173 # Grab the original if it exists
174 if (sizes[-1].getAttribute("label") == "Original"):
175 imgurl = sizes[-1].getAttribute("source")
176 else:
177 print "Failed to get original for photo id " + id
178
179
180 # Free the DOM memory
181 dom.unlink()
182
183 # Grab the image file
184 response = urllib2.urlopen(imgurl)
185 data = response.read()
186
187 # Save the file!
188 fh = open(filename, "w")
189 fh.write(data)
190 fh.close()
191
192 return filename
193 except:
194 print "Failed to retrieve photo id " + id
195
196 ######## Main Application ##########
197 if __name__ == '__main__':
198
199 # The first, and only argument needs to be a directory
200 try:
201 os.chdir(sys.argv[1])
202 except:
203 print "usage: %s directory" % sys.argv[0]
204 sys.exit(1)
205
206 # First things first, see if we have a cached user and auth-token
207 try:
208 cache = open("touchr.frob.cache", "r")
209 config = cPickle.load(cache)
210 cache.close()
211
212 # We don't - get a new one
213 except:
214 (user, token) = froblogin(getfrob(), "read")
215 config = { "version":1 , "user":user, "token":token }
216
217 # Save it for future use
218 cache = open("touchr.frob.cache", "w")
219 cPickle.dump(config, cache)
220 cache.close()
221
222 # Now, construct a query for the list of photo sets
223 url = "http://api.flickr.com/services/rest/?method=flickr.photosets.getList"
224 url += "&user_id=" + config["user"]
225 url = flickrsign(url, config["token"])
226
227 # get the result
228 response = urllib2.urlopen(url)
229
230 # Parse the XML
231 dom = xml.dom.minidom.parse(response)
232
233 # Get the list of Sets
234 sets = dom.getElementsByTagName("photoset")
235
236 # For each set - create a url
237 urls = []
238 for set in sets:
239 pid = set.getAttribute("id")
240 dir = getText(set.getElementsByTagName("title")[0].childNodes)
241
242 # Build the list of photos
243 url = "http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos"
244 url += "&photoset_id=" + pid
245
246 # Append to our list of urls
247 urls.append( (url , dir) )
248
249 # Free the DOM memory
250 dom.unlink()
251
252 # Add the photos which are not in any set
253 url = "http://api.flickr.com/services/rest/?method=flickr.photos.getNotInSet"
254 urls.append( (url, "No Set") )
255
256 # Add the user's Favourites
257 url = "http://api.flickr.com/services/rest/?method=flickr.favorites.getList"
258 urls.append( (url, "Favourites") )
259
260 # Time to get the photos
261 inodes = {}
262 for (url , dir) in urls:
263 # Create the directory
264 try:
265 os.makedirs(dir)
266 except:
267 pass
268
269 # Get 500 results per page
270 url += "&per_page=500"
271 pages = page = 1
272
273 while page <= pages:
274 request = url + "&page=" + str(page)
275
276 # Sign the url
277 request = flickrsign(request, config["token"])
278
279 # Make the request
280 response = urllib2.urlopen(request)
281
282 # Parse the XML
283 dom = xml.dom.minidom.parse(response)
284
285 # Get the total
286 pages = int(dom.getElementsByTagName("photo")[0].parentNode.getAttribute("pages"))
287
288 # Grab the photos
289 for photo in dom.getElementsByTagName("photo"):
290 # Tell the user we're grabbing the file
291 print photo.getAttribute("title").encode("utf8") + " ... in set ... " + dir
292
293 # Grab the id
294 photoid = photo.getAttribute("id")
295
296 # The target
297 target = dir + "/" + photoid + ".jpg"
298
299 # Skip files that exist
300 if os.access(target, os.R_OK):
301 continue
302
303 # Look it up in our dictionary of inodes first
304 if inodes.has_key(photoid) and os.access(inodes[photoid], os.R_OK):
305 # woo, we have it already, use a hard-link
306 os.link(inodes[photoid], target)
307 else:
308 inodes[photoid] = getphoto(photo.getAttribute("id"), config["token"], target)
309
310 # Move on the next page
311 page = page + 1