]> Tony Duckles's Git Repositories (git.nynim.org) - flickrtouchr.git/blob - flickrtouchr.py
first commit
[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[-2].getAttribute("label") == "Original"):
175 imgurl = sizes[-2].getAttribute("source")
176 else:
177 imgurl = sizes[-1].getAttribute("source")
178
179 # Free the DOM memory
180 dom.unlink()
181
182 # Grab the image file
183 response = urllib2.urlopen(imgurl)
184 data = response.read()
185
186 # Save the file!
187 fh = open(filename, "w")
188 fh.write(data)
189 fh.close()
190
191 return filename
192 except:
193 print "Failed to retrieve photo id " + id
194
195 ######## Main Application ##########
196 if __name__ == '__main__':
197
198 # The first, and only argument needs to be a directory
199 try:
200 os.chdir(sys.argv[1])
201 except:
202 print "usage: %s directory" % sys.argv[0]
203 sys.exit(1)
204
205 # First things first, see if we have a cached user and auth-token
206 try:
207 cache = open("touchr.frob.cache", "r")
208 config = cPickle.load(cache)
209 cache.close()
210
211 # We don't - get a new one
212 except:
213 (user, token) = froblogin(getfrob(), "read")
214 config = { "version":1 , "user":user, "token":token }
215
216 # Save it for future use
217 cache = open("touchr.frob.cache", "w")
218 cPickle.dump(config, cache)
219 cache.close()
220
221 # Now, construct a query for the list of photo sets
222 url = "http://api.flickr.com/services/rest/?method=flickr.photosets.getList"
223 url += "&user_id=" + config["user"]
224 url = flickrsign(url, config["token"])
225
226 # get the result
227 response = urllib2.urlopen(url)
228
229 # Parse the XML
230 dom = xml.dom.minidom.parse(response)
231
232 # Get the list of Sets
233 sets = dom.getElementsByTagName("photoset")
234
235 # For each set - create a url
236 urls = []
237 for set in sets:
238 pid = set.getAttribute("id")
239 dir = getText(set.getElementsByTagName("title")[0].childNodes)
240
241 # Build the list of photos
242 url = "http://api.flickr.com/services/rest/?method=flickr.photosets.getPhotos"
243 url += "&photoset_id=" + pid
244
245 # Append to our list of urls
246 urls.append( (url , dir) )
247
248 # Free the DOM memory
249 dom.unlink()
250
251 # Add the photos which are not in any set
252 url = "http://api.flickr.com/services/rest/?method=flickr.photos.getNotInSet"
253 urls.append( (url, "No Set") )
254
255 # Add the user's Favourites
256 url = "http://api.flickr.com/services/rest/?method=flickr.favorites.getList"
257 urls.append( (url, "Favourites") )
258
259 # Time to get the photos
260 inodes = {}
261 for (url , dir) in urls:
262 # Create the directory
263 try:
264 os.makedirs(dir)
265 except:
266 pass
267
268 # Get 500 results per page
269 url += "&per_page=500"
270 pages = page = 1
271
272 while page <= pages:
273 request = url + "&page=" + str(page)
274
275 # Sign the url
276 request = flickrsign(request, config["token"])
277
278 # Make the request
279 response = urllib2.urlopen(request)
280
281 # Parse the XML
282 dom = xml.dom.minidom.parse(response)
283
284 # Get the total
285 pages = int(dom.getElementsByTagName("photo")[0].parentNode.getAttribute("pages"))
286
287 # Grab the photos
288 for photo in dom.getElementsByTagName("photo"):
289 # Tell the user we're grabbing the file
290 print photo.getAttribute("title").encode("utf8") + " ... in set ... " + dir
291
292 # Grab the id
293 photoid = photo.getAttribute("id")
294
295 # The target
296 target = dir + "/" + photoid + ".jpg"
297
298 # Skip files that exist
299 if os.access(target, os.R_OK):
300 continue
301
302 # Look it up in our dictionary of inodes first
303 if inodes.has_key(photoid) and os.access(inodes[photoid], os.R_OK):
304 # woo, we have it already, use a hard-link
305 os.link(inodes[photoid], target)
306 else:
307 inodes[photoid] = getphoto(photo.getAttribute("id"), config["token"], target)
308
309 # Move on the next page
310 page = page + 1