sml-flickr 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (7) hide show
  1. data/History.txt +68 -0
  2. data/LICENSE +20 -0
  3. data/README.txt +74 -0
  4. data/TODO +6 -0
  5. data/lib/flickr.rb +745 -0
  6. data/test/test_flickr.rb +1188 -0
  7. metadata +87 -0
@@ -0,0 +1,68 @@
1
+ == 1.0.9
2
+ * 2 major enhancement
3
+ * Added photoset.photos. Returns PhotoCollection.
4
+ * Added photoset.first_photo. Asks and returns the first photoset's photo
5
+ * 3 minor enhancements
6
+ * Implementation of user.photos_url now uses photosurl API call instead of building it from user.id
7
+ * photoset.getInfo now private and called when needed for the first time.
8
+ * photoset.getPhotos now private and called the first time photoset.photos is called.
9
+
10
+ == 1.0.8 2009-01-25
11
+ * 2 minor enhancements
12
+ * Refactored initialization of PhotoCollection so can be instantiated from response returned by photosets.getPhotos
13
+ * Added photosets.getPhotos. Returns PhotoCollection
14
+
15
+ == 1.0.7 2008-11-26
16
+ * 1 major enhancement
17
+ * When a collection of photos is fetched (e.g. by Flickr#search), a PhotoCollection object is now returned, a type of array, which allows easy access to the pagination information returned by Flickr (e.g. total items, pages, etc). Previously this info was lost
18
+ * Minor enhancements:
19
+ * Refactored parsing of response when getting info about photo (public API is not changed).
20
+ * All returned info is now parsed and stored (previously only some attributes were)
21
+ * A record is kept of whether info has been previously fetched from Flickr (avoids unnecessary extra calls)
22
+ * Improved User#getInfo. No longer makes API call for pretty_url (which was in addition to main getInfo call) or photos_url
23
+ * Bugfixes
24
+ * Fixed Photo#source not to make unnecessary API call
25
+ * Fixed User#url not to make unnecessary API call
26
+
27
+ == 1.0.6 2008-07-30
28
+ * Bugfixes:
29
+ * fixed Flickr#photos when used for searching
30
+ * fixed Flickr::Photo#url to not use usernames (if the user changes their username, the url will wrong)
31
+ * fixed Flickr#related_tags
32
+ * Flickr::Photo#to_s was making unnecessary API calls
33
+ * fixed 'test' Rake task
34
+ * fixed 'gem' Rake task
35
+ * Minor enhancements:
36
+ * added Flickr#search as an alias for Flickr#photos in its search mode
37
+ * added Flickr#recent as an alias for Flickr#photos for recent photos
38
+ * added Flickr::Photo#pretty_url for the URL that includes a username if present
39
+ * added Flickr::Photo#size_url which is like url except 'Medium' works the same as other sizes
40
+ * allow lowercase ('medium') and symbol (:medium) forms for sizes
41
+ * internal code cleanup
42
+
43
+ == 1.0.5 2008-05-12
44
+
45
+ * 1 major change:
46
+ * Updated and refactored Flickr::Group class and Flickr#groups method to work with current Flickr API. Flickr#groups now searches for given group, rather than groups.getActiveList (which no longer exists as Flickr API call)
47
+ * Minor enhancements:
48
+ * Tweaked internals so new client instance isn't created each time new object (e.g. photo, user) is created
49
+ * Improved test coverage
50
+
51
+ == 1.0.4 2008-05-11
52
+
53
+ * 1 major enhancement:
54
+ * Added authentication facility as per current Flickr API
55
+ * 3 minor enhancements:
56
+ * Improved test suite
57
+ * Improved API for creating Flickr objects (old one still works)
58
+ * Protected methods that are only used internally (e.g. request, request_url)
59
+
60
+ == 1.0.3 2008-04-18
61
+
62
+ * Various bugfixes:
63
+ * User instantiation was broken (wrong number of params passed)
64
+ * Instantiating photos failed in several places if single photo returned
65
+ * Photosets#getInfo would always fail as the parameter name should be photoset_id, not photosets_id
66
+ * Removed call to flickr.people.getOnlineList in Flickr#users as that call is no longer in the Flickr API
67
+ * Performance improvements:
68
+ * Url and image source uri now generated without a call to the Flickr API, as per the Flickr API docs
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2007 Scott Raymond, Patrick Plattes, Chris Taggart
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,74 @@
1
+ = flickr
2
+
3
+ http://github.com/ctagg/flickr
4
+
5
+ == DESCRIPTION:
6
+
7
+ An insanely easy interface to the Flickr photo-sharing service. By Scott Raymond. (& updated May 08 by Chris Taggart, http://pushrod.wordpress.com)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ The flickr gem (famously featured in a RubyonRails screencast) had broken with Flickr's new authentication scheme and updated API.
12
+ This has now been largely corrected, though not all current API calls are supported yet.
13
+
14
+ == SYNOPSIS:
15
+
16
+ require 'flickr'
17
+ flickr = Flickr.new('some_flickr_api_key') # create a flickr client (get an API key from http://www.flickr.com/services/api/)
18
+ user = flickr.users('sco@scottraymond.net') # lookup a user
19
+ user.name # get the user's name
20
+ user.location # and location
21
+ user.photos # grab their collection of Photo objects...
22
+ user.groups # ...the groups they're in...
23
+ user.contacts # ...their contacts...
24
+ user.favorites # ...favorite photos...
25
+ user.photosets # ...their photo sets...
26
+ user.tags # ...their tags...
27
+ user.popular_tags # ...and their popular tags
28
+ recentphotos = flickr.photos # get the 100 most recent public photos
29
+ photo = recentphotos.first # or very most recent one
30
+ photo.url # see its URL,
31
+ photo.title # title,
32
+ photo.description # and description,
33
+ photo.owner # and its owner.
34
+ File.open(photo.filename, 'w') do |file|
35
+ file.puts p.file # save the photo to a local file
36
+ end
37
+ flickr.photos.each do |p| # get the last 100 public photos...
38
+ File.open(p.filename, 'w') do |f|
39
+ f.puts p.file('Square') # ...and save a local copy of their square thumbnail
40
+ end
41
+ end
42
+
43
+ == REQUIREMENTS:
44
+
45
+ * Xmlsimple gem
46
+
47
+ == INSTALL:
48
+
49
+ * sudo gem install flickr
50
+
51
+ == LICENSE:
52
+
53
+ (The MIT License)
54
+
55
+ Copyright (c) 2008 Scott Raymond, Patrick Plattes, Chris Taggart
56
+
57
+ Permission is hereby granted, free of charge, to any person obtaining
58
+ a copy of this software and associated documentation files (the
59
+ 'Software'), to deal in the Software without restriction, including
60
+ without limitation the rights to use, copy, modify, merge, publish,
61
+ distribute, sublicense, and/or sell copies of the Software, and to
62
+ permit persons to whom the Software is furnished to do so, subject to
63
+ the following conditions:
64
+
65
+ The above copyright notice and this permission notice shall be
66
+ included in all copies or substantial portions of the Software.
67
+
68
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
69
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
70
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
71
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
72
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
73
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
74
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/TODO ADDED
@@ -0,0 +1,6 @@
1
+ TODO:
2
+ Complete test suite for untested methods
3
+ Bring outdated methods up-to-date
4
+ Write documentation
5
+ Add missing Flickr API call
6
+ Convert dates to ruby dates
@@ -0,0 +1,745 @@
1
+ # = Flickr
2
+ # An insanely easy interface to the Flickr photo-sharing service. By Scott Raymond.
3
+ #
4
+ # Author:: Scott Raymond <sco@redgreenblu.com>
5
+ # Copyright:: Copyright (c) 2005 Scott Raymond <sco@redgreenblu.com>. Additional content by Patrick Plattes and Chris Taggart (http://pushrod.wordpress.com)
6
+ # License:: MIT <http://www.opensource.org/licenses/mit-license.php>
7
+ #
8
+ # BASIC USAGE:
9
+ # require 'flickr'
10
+ # flickr = Flickr.new('some_flickr_api_key') # create a flickr client (get an API key from http://www.flickr.com/services/api/)
11
+ # user = flickr.users('sco@scottraymond.net') # lookup a user
12
+ # user.name # get the user's name
13
+ # user.location # and location
14
+ # user.photos # grab their collection of Photo objects...
15
+ # user.groups # ...the groups they're in...
16
+ # user.contacts # ...their contacts...
17
+ # user.favorites # ...favorite photos...
18
+ # user.photosets # ...their photo sets...
19
+ # user.tags # ...and their tags
20
+ # recentphotos = flickr.photos # get the 100 most recent public photos
21
+ # photo = recentphotos.first # or very most recent one
22
+ # photo.url # see its URL,
23
+ # photo.title # title,
24
+ # photo.description # and description,
25
+ # photo.owner # and its owner.
26
+ # File.open(photo.filename, 'w') do |file|
27
+ # file.puts p.file # save the photo to a local file
28
+ # end
29
+ # flickr.photos.each do |p| # get the last 100 public photos...
30
+ # File.open(p.filename, 'w') do |f|
31
+ # f.puts p.file('Square') # ...and save a local copy of their square thumbnail
32
+ # end
33
+ # end
34
+
35
+
36
+ require 'cgi'
37
+ require 'net/http'
38
+ require 'xmlsimple' unless defined? XmlSimple
39
+ require 'digest/md5'
40
+
41
+ # Flickr client class. Requires an API key
42
+ class Flickr
43
+ attr_reader :api_key, :auth_token
44
+ attr_accessor :user
45
+
46
+ HOST = 'api.flickr.com'
47
+ HOST_URL = 'http://' + HOST
48
+ API_PATH = '/services/rest'
49
+
50
+ # Flickr, annoyingly, uses a number of representations to specify the size
51
+ # of a photo, depending on the context. It gives a label such a "Small" or
52
+ # "Medium" to a size of photo, when returning all possible sizes. However,
53
+ # when generating the uri for the page that features that size of photo, or
54
+ # the source url for the image itself it uses a single letter. Bizarrely,
55
+ # these letters are different depending on whether you want the Flickr page
56
+ # for the photo or the source uri -- e.g. a "Small" photo (240 pixels on its
57
+ # longest side) may be viewed at
58
+ # "http://www.flickr.com/photos/sco/2397458775/sizes/s/"
59
+ # but its source is at
60
+ # "http://farm4.static.flickr.com/3118/2397458775_2ec2ddc324_m.jpg".
61
+ # The VALID_SIZES hash associates the correct letter with a label
62
+ VALID_SIZES = { "Square" => ["s", "sq"],
63
+ "Thumbnail" => ["t", "t"],
64
+ "Small" => ["m", "s"],
65
+ "Medium" => [nil, "m"],
66
+ "Large" => ["b", "l"]
67
+ }
68
+
69
+ # To use the Flickr API you need an api key
70
+ # (see http://www.flickr.com/services/api/misc.api_keys.html), and the flickr
71
+ # client object shuld be initialized with this. You'll also need a shared
72
+ # secret code if you want to use authentication (e.g. to get a user's
73
+ # private photos)
74
+ # There are two ways to initialize the Flickr client. The preferred way is with
75
+ # a hash of params, e.g. 'api_key' => 'your_api_key', 'shared_secret' =>
76
+ # 'shared_secret_code'. The older (deprecated) way is to pass an ordered series of
77
+ # arguments. This is provided for continuity only, as several of the arguments
78
+ # are no longer usable ('email', 'password')
79
+ def initialize(api_key_or_params=nil, email=nil, password=nil, shared_secret=nil)
80
+ @host = HOST_URL
81
+ @api = API_PATH
82
+ if api_key_or_params.is_a?(Hash)
83
+ @api_key = api_key_or_params['api_key']
84
+ @shared_secret = api_key_or_params['shared_secret']
85
+ @auth_token = api_key_or_params['auth_token']
86
+ else
87
+ @api_key = api_key_or_params
88
+ @shared_secret = shared_secret
89
+ login(email, password) if email and password
90
+ end
91
+ end
92
+
93
+ # Gets authentication token given a Flickr frob, which is returned when user
94
+ # allows access to their account for the application with the api_key which
95
+ # made the request
96
+ def get_token_from(frob)
97
+ auth_response = request("auth.getToken", :frob => frob)['auth']
98
+ @auth_token = auth_response['token']
99
+ @user = User.new( 'id' => auth_response['user']['nsid'],
100
+ 'username' => auth_response['user']['username'],
101
+ 'name' => auth_response['user']['fullname'],
102
+ 'client' => self)
103
+ @auth_token
104
+ end
105
+
106
+ # Stores authentication credentials to use on all subsequent calls.
107
+ # If authentication succeeds, returns a User object.
108
+ # NB This call is no longer in API and will result in an error if called
109
+ def login(email='', password='')
110
+ @email = email
111
+ @password = password
112
+ user = request('test.login')['user'] rescue fail
113
+ @user = User.new(user['id'], nil, nil, nil, @api_key)
114
+ end
115
+
116
+ # Implements flickr.urls.lookupGroup and flickr.urls.lookupUser
117
+ def find_by_url(url)
118
+ response = urls_lookupUser('url'=>url) rescue urls_lookupGroup('url'=>url) rescue nil
119
+ (response['user']) ? User.new(response['user']['id'], nil, nil, nil, @api_key) : Group.new(response['group']['id'], @api_key) unless response.nil?
120
+ end
121
+
122
+ # Implements flickr.photos.getRecent and flickr.photos.search
123
+ def photos(*criteria)
124
+ criteria ? photos_search(*criteria) : recent
125
+ end
126
+
127
+ # flickr.photos.getRecent
128
+ # 100 newest photos from everyone
129
+ def recent
130
+ photos_request('photos.getRecent')
131
+ end
132
+
133
+ def photos_search(params={})
134
+ photos_request('photos.search', params)
135
+ end
136
+ alias_method :search, :photos_search
137
+
138
+ # Gets public photos with a given tag
139
+ def tag(tag)
140
+ photos('tags'=>tag)
141
+ end
142
+
143
+ # Implements flickr.people.findByEmail and flickr.people.findByUsername.
144
+ def users(lookup=nil)
145
+ user = people_findByEmail('find_email'=>lookup)['user'] rescue people_findByUsername('username'=>lookup)['user']
146
+ return User.new("id" => user["nsid"], "username" => user["username"], "client" => self)
147
+ end
148
+
149
+ # Implements flickr.groups.search
150
+ def groups(group_name, options={})
151
+ collection = groups_search({"text" => group_name}.merge(options))['groups']['group']
152
+ collection = [collection] if collection.is_a? Hash
153
+
154
+ collection.collect { |group| Group.new( "id" => group['nsid'],
155
+ "name" => group['name'],
156
+ "eighteenplus" => group['eighteenplus'],
157
+ "client" => self) }
158
+ end
159
+
160
+ def photoset(photoset_id)
161
+ Photoset.new(photoset_id, @api_key)
162
+ end
163
+
164
+ # Implements flickr.tags.getRelated
165
+ def related_tags(tag)
166
+ tags_getRelated('tag'=>tag)['tags']['tag']
167
+ end
168
+
169
+ # Implements flickr.photos.licenses.getInfo
170
+ def licenses
171
+ photos_licenses_getInfo['licenses']['license']
172
+ end
173
+
174
+ # Returns url for user to login in to Flickr to authenticate app for a user
175
+ def login_url(perms)
176
+ "http://flickr.com/services/auth/?api_key=#{@api_key}&perms=#{perms}&api_sig=#{signature_from('api_key'=>@api_key, 'perms' => perms)}"
177
+ end
178
+
179
+ # Implements everything else.
180
+ # Any method not defined explicitly will be passed on to the Flickr API,
181
+ # and return an XmlSimple document. For example, Flickr#test_echo is not
182
+ # defined, so it will pass the call to the flickr.test.echo method.
183
+ def method_missing(method_id, params={})
184
+ request(method_id.id2name.gsub(/_/, '.'), params)
185
+ end
186
+
187
+ # Does an HTTP GET on a given URL and returns the response body
188
+ def http_get(url)
189
+ Net::HTTP.get_response(URI.parse(url)).body.to_s
190
+ end
191
+
192
+ # Takes a Flickr API method name and set of parameters; returns an XmlSimple object with the response
193
+ def request(method, params={})
194
+ url = request_url(method, params)
195
+ response = XmlSimple.xml_in(http_get(url), { 'ForceArray' => false })
196
+ raise response['err']['msg'] if response['stat'] != 'ok'
197
+ response
198
+ end
199
+
200
+ # acts like request but returns a PhotoCollection (a list of Photo objects)
201
+ def photos_request(method, params={})
202
+ photos = request(method, params)
203
+ PhotoCollection.new(photos, @api_key)
204
+ end
205
+
206
+ # Builds url for Flickr API REST request from given the flickr method name
207
+ # (exclusing the 'flickr.' that begins each method call) and params (where
208
+ # applicable) which should be supplied as a Hash (e.g 'user_id' => "foo123")
209
+ def request_url(method, params={})
210
+ method = 'flickr.' + method
211
+ url = "#{@host}#{@api}/?api_key=#{@api_key}&method=#{method}"
212
+ params.merge!('api_key' => @api_key, 'method' => method, 'auth_token' => @auth_token)
213
+ signature = signature_from(params)
214
+
215
+ url = "#{@host}#{@api}/?" + params.merge('api_sig' => signature).collect { |k,v| "#{k}=" + CGI::escape(v.to_s) unless v.nil? }.compact.join("&")
216
+ end
217
+
218
+ def signature_from(params={})
219
+ return unless @shared_secret # don't both getting signature if no shared_secret
220
+ request_str = params.reject {|k,v| v.nil?}.collect {|p| "#{p[0].to_s}#{p[1]}"}.sort.join # build key value pairs, sort in alpha order then join them, ignoring those with nil value
221
+ return Digest::MD5.hexdigest("#{@shared_secret}#{request_str}")
222
+ end
223
+
224
+ # A collection of photos is returned as a PhotoCollection, a subclass of Array.
225
+ # This allows us to retain the pagination info returned by Flickr and make it
226
+ # accessible in a friendly way
227
+ class PhotoCollection < Array
228
+ attr_reader :page, :pages, :perpage, :total
229
+
230
+ # builds a PhotoCollection from given params, such as those returned from
231
+ # photos.search API call. Note all the info is contained in the value of
232
+ # the first (and only) key-value pair of the response. The key will vary
233
+ # depending on the original object the photos are related to (e.g 'photos',
234
+ # 'photoset', etc)
235
+ def initialize(photos_api_response={}, api_key=nil)
236
+ photos = photos_api_response.values.first
237
+ [ "page", "pages", "perpage", "total" ].each { |i| instance_variable_set("@#{i}", photos[i])}
238
+ collection = photos['photo'] || []
239
+ collection = [collection] if collection.is_a? Hash
240
+ collection.each { |photo| self << Photo.new(photo.delete('id'), api_key, photo) }
241
+ end
242
+ end
243
+
244
+ # Todo:
245
+ # logged_in?
246
+ # if logged in:
247
+ # flickr.blogs.getList
248
+ # flickr.favorites.add
249
+ # flickr.favorites.remove
250
+ # flickr.groups.browse
251
+ # flickr.photos.getCounts
252
+ # flickr.photos.getNotInSet
253
+ # flickr.photos.getUntagged
254
+ # flickr.photosets.create
255
+ # flickr.photosets.orderSets
256
+ # flickr.test.login
257
+ # uploading
258
+ class User
259
+
260
+ attr_reader :client, :id, :name, :location, :photos_url, :url, :count, :firstdate, :firstdatetaken
261
+
262
+ # A Flickr::User can be instantiated in two ways. The old (deprecated)
263
+ # method is with an ordered series of values. The new method is with a
264
+ # params Hash, which is easier when a variable number of params are
265
+ # supplied, which is the case here, and also avoids having to constantly
266
+ # supply nil values for the email and password, which are now irrelevant
267
+ # as authentication is no longer done this way.
268
+ # An associated flickr client will also be generated if an api key is
269
+ # passed among the arguments or in the params hash. Alternatively, and
270
+ # most likely, an existing client object may be passed in the params hash
271
+ # (e.g. 'client' => some_existing_flickr_client_object), and this is
272
+ # what happends when users are initlialized as the result of a method
273
+ # called on the flickr client (e.g. flickr.users)
274
+ def initialize(id_or_params_hash=nil, username=nil, email=nil, password=nil, api_key=nil)
275
+ if id_or_params_hash.is_a?(Hash)
276
+ id_or_params_hash.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
277
+ else
278
+ @id = id_or_params_hash
279
+ @username = username
280
+ @email = email
281
+ @password = password
282
+ @api_key = api_key
283
+ end
284
+ @client ||= Flickr.new('api_key' => @api_key, 'shared_secret' => @shared_secret, 'auth_token' => @auth_token) if @api_key
285
+ @client.login(@email, @password) if @email and @password # this is now irrelevant as Flickr API no longer supports authentication this way
286
+ end
287
+
288
+ def username
289
+ @username.nil? ? getInfo.username : @username
290
+ end
291
+ def name
292
+ @name.nil? ? getInfo.name : @name
293
+ end
294
+ def location
295
+ @location.nil? ? getInfo.location : @location
296
+ end
297
+ def count
298
+ @count.nil? ? getInfo.count : @count
299
+ end
300
+ def firstdate
301
+ @firstdate.nil? ? getInfo.firstdate : @firstdate
302
+ end
303
+ def firstdatetaken
304
+ @firstdatetaken.nil? ? getInfo.firstdatetaken : @firstdatetaken
305
+ end
306
+
307
+ def photos_url
308
+ @photos_url || getInfo.photos_url
309
+ end
310
+
311
+ # Builds url for user's profile page as per
312
+ # http://www.flickr.com/services/api/misc.urls.html
313
+ def url
314
+ "http://www.flickr.com/people/#{id}/"
315
+ end
316
+
317
+ def pretty_url
318
+ @pretty_url ||= @client.urls_getUserProfile('user_id'=>@id)['user']['url']
319
+ end
320
+
321
+ # Implements flickr.people.getPublicGroups
322
+ def groups
323
+ collection = @client.people_getPublicGroups('user_id'=>@id)['groups']['group']
324
+ collection = [collection] if collection.is_a? Hash
325
+ collection.collect { |group| Group.new( "id" => group['nsid'],
326
+ "name" => group['name'],
327
+ "eighteenplus" => group['eighteenplus'],
328
+ "client" => @client) }
329
+ end
330
+
331
+ # Implements flickr.people.getPublicPhotos. Options hash allows you to add
332
+ # extra restrictions as per flickr.people.getPublicPhotos docs, e.g.
333
+ # user.photos('per_page' => '25', 'extras' => 'date_taken')
334
+ def photos(options={})
335
+ @client.photos_request('people.getPublicPhotos', {'user_id' => @id}.merge(options))
336
+ # what about non-public photos?
337
+ end
338
+
339
+ # Gets photos with a given tag
340
+ def tag(tag)
341
+ @client.photos('user_id'=>@id, 'tags'=>tag)
342
+ end
343
+
344
+ # Implements flickr.contacts.getPublicList and flickr.contacts.getList
345
+ def contacts
346
+ @client.contacts_getPublicList('user_id'=>@id)['contacts']['contact'].collect { |contact| User.new(contact['nsid'], contact['username'], nil, nil, @api_key) }
347
+ #or
348
+ end
349
+
350
+ # Implements flickr.favorites.getPublicList
351
+ def favorites
352
+ @client.photos_request('favorites.getPublicList', 'user_id' => @id)
353
+ end
354
+
355
+ # Implements flickr.photosets.getList
356
+ def photosets
357
+ @client.photosets_getList('user_id'=>@id)['photosets']['photoset'].collect { |photoset| Photoset.new(photoset['id'], @api_key) }
358
+ end
359
+
360
+ # Implements flickr.tags.getListUser
361
+ def tags
362
+ @client.tags_getListUser('user_id'=>@id)['who']['tags']['tag'].collect { |tag| tag }
363
+ end
364
+
365
+ # Implements flickr.tags.getListUserPopular
366
+ def popular_tags(count = 10)
367
+ @client.tags_getListUserPopular('user_id'=>@id, 'count'=> count)['who']['tags']['tag'].each { |tag_score| tag_score["tag"] = tag_score.delete("content") }
368
+ end
369
+
370
+ # Implements flickr.photos.getContactsPublicPhotos and flickr.photos.getContactsPhotos
371
+ def contactsPhotos
372
+ @client.photos_request('photos.getContactsPublicPhotos', 'user_id' => @id)
373
+ end
374
+
375
+ def to_s
376
+ @name
377
+ end
378
+
379
+ private
380
+
381
+ # Implements flickr.people.getInfo, flickr.urls.getUserPhotos, and flickr.urls.getUserProfile
382
+ def getInfo
383
+ unless @info
384
+ @info = @client.people_getInfo('user_id'=>@id)['person']
385
+ @username = @info['username']
386
+ @name = @info['realname']
387
+ @location = @info['location']
388
+ @photos_url = @info['photosurl']
389
+ @count = @info['photos']['count']
390
+ @firstdate = @info['photos']['firstdate']
391
+ @firstdatetaken = @info['photos']['firstdatetaken']
392
+ end
393
+ self
394
+ end
395
+
396
+ end
397
+
398
+ class Photo
399
+
400
+ attr_reader :id, :client, :title
401
+
402
+ def initialize(id=nil, api_key=nil, extra_params={})
403
+ @id = id
404
+ @api_key = api_key
405
+ extra_params.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
406
+ @client = Flickr.new @api_key
407
+ end
408
+
409
+ # Allows access to all photos instance variables through hash like
410
+ # interface, e.g. photo["datetaken"] returns @datetaken instance
411
+ # variable. Useful for accessing any weird and wonderful parameter
412
+ # that may have been returned by Flickr when finding the photo,
413
+ # e.g. those returned by the extras argument in
414
+ # flickr.people.getPublicPhotos
415
+ def [](param_name)
416
+ instance_variable_get("@#{param_name}")
417
+ end
418
+
419
+ def title
420
+ @title.nil? ? getInfo("title") : @title
421
+ end
422
+
423
+ # Returns the owner of the photo as a Flickr::User. If we have no info
424
+ # about the owner, we make an API call to get it. If we already have
425
+ # the owner's id, create a user based on that. Either way, we cache the
426
+ # result so we don't need to check again
427
+ def owner
428
+ case @owner
429
+ when Flickr::User
430
+ @owner
431
+ when String
432
+ @owner = Flickr::User.new(@owner, nil, nil, nil, @api_key)
433
+ else
434
+ getInfo("owner")
435
+ end
436
+ end
437
+
438
+ def server
439
+ @server.nil? ? getInfo("server") : @server
440
+ end
441
+
442
+ def isfavorite
443
+ @isfavorite.nil? ? getInfo("isfavorite") : @isfavorite
444
+ end
445
+
446
+ def license
447
+ @license.nil? ? getInfo("license") : @license
448
+ end
449
+
450
+ def rotation
451
+ @rotation.nil? ? getInfo("rotation") : @rotation
452
+ end
453
+
454
+ def description
455
+ @description || getInfo("description")
456
+ end
457
+
458
+ def notes
459
+ @notes.nil? ? getInfo("notes") : @notes
460
+ end
461
+
462
+ # Returns the URL for the photo size page
463
+ # defaults to 'Medium'
464
+ # other valid sizes are in the VALID_SIZES hash
465
+ def size_url(size='Medium')
466
+ uri_for_photo_from_self(size) || sizes(size)['url']
467
+ end
468
+
469
+ # converts string or symbol size to a capitalized string
470
+ def normalize_size(size)
471
+ size ? size.to_s.capitalize : size
472
+ end
473
+
474
+ # the URL for the main photo page
475
+ # if getInfo has already been called, this will return the pretty url
476
+ #
477
+ # for historical reasons, an optional size can be given
478
+ # 'Medium' returns the regular url; any other size returns a size page
479
+ # use size_url instead
480
+ def url(size = nil)
481
+ if normalize_size(size) != 'Medium'
482
+ size_url(size)
483
+ else
484
+ @url || uri_for_photo_from_self
485
+ end
486
+ end
487
+
488
+ # the 'pretty' url for a photo
489
+ # (if the user has set up a custom name)
490
+ # eg, http://flickr.com/photos/granth/2584402507/ instead of
491
+ # http://flickr.com/photos/23386158@N00/2584402507/
492
+ def pretty_url
493
+ @url || getInfo("pretty_url")
494
+ end
495
+
496
+ # Returns the URL for the image (default or any specified size)
497
+ def source(size='Medium')
498
+ image_source_uri_from_self(size) || sizes(size)['source']
499
+ end
500
+
501
+ # Returns the photo file data itself, in any specified size. Example: File.open(photo.title, 'w') { |f| f.puts photo.file }
502
+ def file(size='Medium')
503
+ Net::HTTP.get_response(URI.parse(source(size))).body
504
+ end
505
+
506
+ # Unique filename for the image, based on the Flickr NSID
507
+ def filename
508
+ "#{@id}.jpg"
509
+ end
510
+
511
+ # Implements flickr.photos.getContext
512
+ def context
513
+ context = @client.photos_getContext('photo_id'=>@id)
514
+ @previousPhoto = Photo.new(context['prevphoto'].delete('id'), @api_key, context['prevphoto']) if context['prevphoto']['id']!='0'
515
+ @nextPhoto = Photo.new(context['nextphoto'].delete('id'), @api_key, context['nextphoto']) if context['nextphoto']['id']!='0'
516
+ return [@previousPhoto, @nextPhoto]
517
+ end
518
+
519
+ # Implements flickr.photos.getExif
520
+ def exif
521
+ @client.photos_getExif('photo_id'=>@id)['photo']
522
+ end
523
+
524
+ # Implements flickr.photos.getPerms
525
+ def permissions
526
+ @client.photos_getPerms('photo_id'=>@id)['perms']
527
+ end
528
+
529
+ # Implements flickr.photos.getSizes
530
+ def sizes(size=nil)
531
+ size = normalize_size(size)
532
+ sizes = @client.photos_getSizes('photo_id'=>@id)['sizes']['size']
533
+ sizes = sizes.find{|asize| asize['label']==size} if size
534
+ return sizes
535
+ end
536
+
537
+ def vertical?
538
+ @medium_size ||= self.sizes('Medium')
539
+ @medium_size['height'] > @medium_size['width']
540
+ end
541
+
542
+ # flickr.tags.getListPhoto
543
+ def tags
544
+ @client.tags_getListPhoto('photo_id'=>@id)['photo']['tags']
545
+ end
546
+
547
+ # Implements flickr.photos.notes.add
548
+ def add_note(note)
549
+ end
550
+
551
+ # Implements flickr.photos.setDates
552
+ def dates=(dates)
553
+ end
554
+
555
+ # Implements flickr.photos.setPerms
556
+ def perms=(perms)
557
+ end
558
+
559
+ # Implements flickr.photos.setTags
560
+ def tags=(tags)
561
+ end
562
+
563
+ # Implements flickr.photos.setMeta
564
+ def title=(title)
565
+ end
566
+ def description=(title)
567
+ end
568
+
569
+ # Implements flickr.photos.addTags
570
+ def add_tag(tag)
571
+ end
572
+
573
+ # Implements flickr.photos.removeTag
574
+ def remove_tag(tag)
575
+ end
576
+
577
+ # Implements flickr.photos.transform.rotate
578
+ def rotate
579
+ end
580
+
581
+ # Implements flickr.blogs.postPhoto
582
+ def postToBlog(blog_id, title='', description='')
583
+ @client.blogs_postPhoto('photo_id'=>@id, 'title'=>title, 'description'=>description)
584
+ end
585
+
586
+ # Implements flickr.photos.notes.delete
587
+ def deleteNote(note_id)
588
+ end
589
+
590
+ # Implements flickr.photos.notes.edit
591
+ def editNote(note_id)
592
+ end
593
+
594
+ # Converts the Photo to a string by returning its title
595
+ def to_s
596
+ title
597
+ end
598
+
599
+ private
600
+
601
+ # Implements flickr.photos.getInfo
602
+ def getInfo(attrib="")
603
+ return instance_variable_get("@#{attrib}") if @got_info
604
+ info = @client.photos_getInfo('photo_id'=>@id)['photo']
605
+ @got_info = true
606
+ info.each { |k,v| instance_variable_set("@#{k}", v)}
607
+ @owner = User.new(info['owner']['nsid'], info['owner']['username'], nil, nil, @api_key)
608
+ @tags = info['tags']['tag']
609
+ @notes = info['notes']['note']#.collect { |note| Note.new(note.id) }
610
+ @url = info['urls']['url']['content'] # assumes only one url
611
+ instance_variable_get("@#{attrib}")
612
+ end
613
+
614
+ # Builds source uri of image from params (often returned from other
615
+ # methods, e.g. User#photos). As specified at:
616
+ # http://www.flickr.com/services/api/misc.urls.html. If size is given
617
+ # should be one the keys in the VALID_SIZES hash, i.e.
618
+ # "Square", "Thumbnail", "Medium", "Large", "Original", "Small" (These
619
+ # are the values returned by flickr.photos.getSizes).
620
+ # If no size is given the uri for "Medium"-size image, i.e. with width
621
+ # of 500 is returned
622
+ # TODO: Handle "Original" size
623
+ def image_source_uri_from_self(size=nil)
624
+ return unless @farm&&@server&&@id&&@secret
625
+ s_size = VALID_SIZES[normalize_size(size)] # get the short letters array corresponding to the size
626
+ s_size = s_size&&s_size[0] # the first element of this array is used to build the source uri
627
+ if s_size.nil?
628
+ "http://farm#{@farm}.static.flickr.com/#{@server}/#{@id}_#{@secret}.jpg"
629
+ else
630
+ "http://farm#{@farm}.static.flickr.com/#{@server}/#{@id}_#{@secret}_#{s_size}.jpg"
631
+ end
632
+ end
633
+
634
+ # Builds uri of Flickr page for photo. By default returns the main
635
+ # page for the photo, but if passed a size will return the simplified
636
+ # flickr page featuring the given size of the photo
637
+ # TODO: Handle "Original" size
638
+ def uri_for_photo_from_self(size=nil)
639
+ return unless @owner&&@id
640
+ size = normalize_size(size)
641
+ s_size = VALID_SIZES[size] # get the short letters array corresponding to the size
642
+ s_size = s_size&&s_size[1] # the second element of this array is used to build the uri of the flickr page for this size
643
+ "http://www.flickr.com/photos/#{owner.id}/#{@id}" + (s_size ? "/sizes/#{s_size}/" : "")
644
+ end
645
+ end
646
+
647
+ # Todo:
648
+ # flickr.groups.pools.add
649
+ # flickr.groups.pools.getContext
650
+ # flickr.groups.pools.getGroups
651
+ # flickr.groups.pools.getPhotos
652
+ # flickr.groups.pools.remove
653
+ class Group
654
+ attr_reader :id, :client, :description, :name, :eighteenplus, :members, :online, :privacy, :url#, :chatid, :chatcount
655
+
656
+ def initialize(id_or_params_hash=nil, api_key=nil)
657
+ if id_or_params_hash.is_a?(Hash)
658
+ id_or_params_hash.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
659
+ else
660
+ @id = id_or_params_hash
661
+ @api_key = api_key
662
+ @client = Flickr.new @api_key
663
+ end
664
+ end
665
+
666
+ # Implements flickr.groups.getInfo and flickr.urls.getGroup
667
+ # private, once we can call it as needed
668
+ def getInfo
669
+ info = @client.groups_getInfo('group_id'=>@id)['group']
670
+ @name = info['name']
671
+ @members = info['members']
672
+ @online = info['online']
673
+ @privacy = info['privacy']
674
+ # @chatid = info['chatid']
675
+ # @chatcount = info['chatcount']
676
+ @url = @client.urls_getGroup('group_id'=>@id)['group']['url']
677
+ self
678
+ end
679
+
680
+ end
681
+
682
+ # Todo:
683
+ # flickr.photosets.delete
684
+ # flickr.photosets.editMeta
685
+ # flickr.photosets.editPhotos
686
+ # flickr.photosets.getContext
687
+ # flickr.photosets.getInfo
688
+ # flickr.photosets.getPhotos
689
+ class Photoset
690
+
691
+ attr_reader :id, :client, :owner, :primary, :photos, :title, :description, :url
692
+
693
+ def initialize(id=nil, api_key=nil)
694
+ @id = id
695
+ @api_key = api_key
696
+ @client = Flickr.new @api_key
697
+ end
698
+
699
+ def owner
700
+ @owner || getInfo.owner
701
+ end
702
+
703
+ def primary
704
+ @primary || getInfo.primary
705
+ end
706
+
707
+ def title
708
+ @title || getInfo.title
709
+ end
710
+
711
+ def url
712
+ @url || getInfo.url
713
+ end
714
+
715
+ def photos
716
+ @photos ||= getPhotos
717
+ end
718
+
719
+ def first_photo
720
+ @first_photo ||= getFirstPhoto
721
+ end
722
+
723
+ private
724
+ def getInfo
725
+ unless @info
726
+ @info = @client.photosets_getInfo('photoset_id'=>@id)['photoset']
727
+ @owner = User.new(@info['owner'], nil, nil, nil, @api_key)
728
+ @primary = @info['primary']
729
+ @title = @info['title']
730
+ @description = @info['description']
731
+ @url = "#{@owner.photos_url}sets/#{@id}/"
732
+ end
733
+ self
734
+ end
735
+
736
+ def getPhotos
737
+ @client.photos_request('photosets.getPhotos', {'photoset_id' => @id})
738
+ end
739
+
740
+ def getFirstPhoto
741
+ @client.photos_request('photosets.getPhotos', {'photoset_id' => @id, :per_page => 1}).first
742
+ end
743
+ end
744
+
745
+ end