thecocktail-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 +739 -0
  6. data/test/test_flickr.rb +1180 -0
  7. metadata +68 -0
data/History.txt ADDED
@@ -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.
data/README.txt ADDED
@@ -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
data/lib/flickr.rb ADDED
@@ -0,0 +1,739 @@
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'
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_URL = 'http://api.flickr.com'
47
+ API_PATH = '/services/rest'
48
+
49
+ # Flickr, annoyingly, uses a number of representations to specify the size
50
+ # of a photo, depending on the context. It gives a label such a "Small" or
51
+ # "Medium" to a size of photo, when returning all possible sizes. However,
52
+ # when generating the uri for the page that features that size of photo, or
53
+ # the source url for the image itself it uses a single letter. Bizarrely,
54
+ # these letters are different depending on whether you want the Flickr page
55
+ # for the photo or the source uri -- e.g. a "Small" photo (240 pixels on its
56
+ # longest side) may be viewed at
57
+ # "http://www.flickr.com/photos/sco/2397458775/sizes/s/"
58
+ # but its source is at
59
+ # "http://farm4.static.flickr.com/3118/2397458775_2ec2ddc324_m.jpg".
60
+ # The VALID_SIZES hash associates the correct letter with a label
61
+ VALID_SIZES = { "Square" => ["s", "sq"],
62
+ "Thumbnail" => ["t", "t"],
63
+ "Small" => ["m", "s"],
64
+ "Medium" => [nil, "m"],
65
+ "Large" => ["b", "l"]
66
+ }
67
+
68
+ # To use the Flickr API you need an api key
69
+ # (see http://www.flickr.com/services/api/misc.api_keys.html), and the flickr
70
+ # client object shuld be initialized with this. You'll also need a shared
71
+ # secret code if you want to use authentication (e.g. to get a user's
72
+ # private photos)
73
+ # There are two ways to initialize the Flickr client. The preferred way is with
74
+ # a hash of params, e.g. 'api_key' => 'your_api_key', 'shared_secret' =>
75
+ # 'shared_secret_code'. The older (deprecated) way is to pass an ordered series of
76
+ # arguments. This is provided for continuity only, as several of the arguments
77
+ # are no longer usable ('email', 'password')
78
+ def initialize(api_key_or_params=nil, email=nil, password=nil, shared_secret=nil)
79
+ @host = HOST_URL
80
+ @api = API_PATH
81
+ if api_key_or_params.is_a?(Hash)
82
+ @api_key = api_key_or_params['api_key']
83
+ @shared_secret = api_key_or_params['shared_secret']
84
+ @auth_token = api_key_or_params['auth_token']
85
+ else
86
+ @api_key = api_key_or_params
87
+ @shared_secret = shared_secret
88
+ login(email, password) if email and password
89
+ end
90
+ end
91
+
92
+ # Gets authentication token given a Flickr frob, which is returned when user
93
+ # allows access to their account for the application with the api_key which
94
+ # made the request
95
+ def get_token_from(frob)
96
+ auth_response = request("auth.getToken", :frob => frob)['auth']
97
+ @auth_token = auth_response['token']
98
+ @user = User.new( 'id' => auth_response['user']['nsid'],
99
+ 'username' => auth_response['user']['username'],
100
+ 'name' => auth_response['user']['fullname'],
101
+ 'client' => self)
102
+ @auth_token
103
+ end
104
+
105
+ # Stores authentication credentials to use on all subsequent calls.
106
+ # If authentication succeeds, returns a User object.
107
+ # NB This call is no longer in API and will result in an error if called
108
+ def login(email='', password='')
109
+ @email = email
110
+ @password = password
111
+ user = request('test.login')['user'] rescue fail
112
+ @user = User.new(user['id'], nil, nil, nil, @api_key)
113
+ end
114
+
115
+ # Implements flickr.urls.lookupGroup and flickr.urls.lookupUser
116
+ def find_by_url(url)
117
+ response = urls_lookupUser('url'=>url) rescue urls_lookupGroup('url'=>url) rescue nil
118
+ (response['user']) ? User.new(response['user']['id'], nil, nil, nil, @api_key) : Group.new(response['group']['id'], @api_key) unless response.nil?
119
+ end
120
+
121
+ # Implements flickr.photos.getRecent and flickr.photos.search
122
+ def photos(*criteria)
123
+ criteria ? photos_search(*criteria) : recent
124
+ end
125
+
126
+ # flickr.photos.getRecent
127
+ # 100 newest photos from everyone
128
+ def recent
129
+ photos_request('photos.getRecent')
130
+ end
131
+
132
+ def photos_search(params={})
133
+ photos_request('photos.search', params)
134
+ end
135
+ alias_method :search, :photos_search
136
+
137
+ # Gets public photos with a given tag
138
+ def tag(tag)
139
+ photos('tags'=>tag)
140
+ end
141
+
142
+ # Implements flickr.people.findByEmail and flickr.people.findByUsername.
143
+ def users(lookup=nil)
144
+ user = people_findByEmail('find_email'=>lookup)['user'] rescue people_findByUsername('username'=>lookup)['user']
145
+ return User.new("id" => user["nsid"], "username" => user["username"], "client" => self)
146
+ end
147
+
148
+ # Implements flickr.groups.search
149
+ def groups(group_name, options={})
150
+ collection = groups_search({"text" => group_name}.merge(options))['groups']['group']
151
+ collection = [collection] if collection.is_a? Hash
152
+
153
+ collection.collect { |group| Group.new( "id" => group['nsid'],
154
+ "name" => group['name'],
155
+ "eighteenplus" => group['eighteenplus'],
156
+ "client" => self) }
157
+ end
158
+
159
+ def photoset(photoset_id)
160
+ Photoset.new(photoset_id, @api_key)
161
+ end
162
+
163
+ # Implements flickr.tags.getRelated
164
+ def related_tags(tag)
165
+ tags_getRelated('tag'=>tag)['tags']['tag']
166
+ end
167
+
168
+ # Implements flickr.photos.licenses.getInfo
169
+ def licenses
170
+ photos_licenses_getInfo['licenses']['license']
171
+ end
172
+
173
+ # Returns url for user to login in to Flickr to authenticate app for a user
174
+ def login_url(perms)
175
+ "http://flickr.com/services/auth/?api_key=#{@api_key}&perms=#{perms}&api_sig=#{signature_from('api_key'=>@api_key, 'perms' => perms)}"
176
+ end
177
+
178
+ # Implements everything else.
179
+ # Any method not defined explicitly will be passed on to the Flickr API,
180
+ # and return an XmlSimple document. For example, Flickr#test_echo is not
181
+ # defined, so it will pass the call to the flickr.test.echo method.
182
+ def method_missing(method_id, params={})
183
+ request(method_id.id2name.gsub(/_/, '.'), params)
184
+ end
185
+
186
+ # Does an HTTP GET on a given URL and returns the response body
187
+ def http_get(url)
188
+ Net::HTTP.get_response(URI.parse(url)).body.to_s
189
+ end
190
+
191
+ # Takes a Flickr API method name and set of parameters; returns an XmlSimple object with the response
192
+ def request(method, params={})
193
+ url = request_url(method, params)
194
+ response = XmlSimple.xml_in(http_get(url), { 'ForceArray' => false })
195
+ raise response['err']['msg'] if response['stat'] != 'ok'
196
+ response
197
+ end
198
+
199
+ # acts like request but returns a PhotoCollection (a list of Photo objects)
200
+ def photos_request(method, params={})
201
+ photos = request(method, params)
202
+ PhotoCollection.new(photos, @api_key)
203
+ end
204
+
205
+ # Builds url for Flickr API REST request from given the flickr method name
206
+ # (exclusing the 'flickr.' that begins each method call) and params (where
207
+ # applicable) which should be supplied as a Hash (e.g 'user_id' => "foo123")
208
+ def request_url(method, params={})
209
+ method = 'flickr.' + method
210
+ url = "#{@host}#{@api}/?api_key=#{@api_key}&method=#{method}"
211
+ params.merge!('api_key' => @api_key, 'method' => method, 'auth_token' => @auth_token)
212
+ signature = signature_from(params)
213
+
214
+ url = "#{@host}#{@api}/?" + params.merge('api_sig' => signature).collect { |k,v| "#{k}=" + CGI::escape(v.to_s) unless v.nil? }.compact.join("&")
215
+ end
216
+
217
+ def signature_from(params={})
218
+ return unless @shared_secret # don't both getting signature if no shared_secret
219
+ 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
220
+ return Digest::MD5.hexdigest("#{@shared_secret}#{request_str}")
221
+ end
222
+
223
+ # A collection of photos is returned as a PhotoCollection, a subclass of Array.
224
+ # This allows us to retain the pagination info returned by Flickr and make it
225
+ # accessible in a friendly way
226
+ class PhotoCollection < Array
227
+ attr_reader :page, :pages, :perpage, :total
228
+
229
+ # builds a PhotoCollection from given params, such as those returned from
230
+ # photos.search API call. Note all the info is contained in the value of
231
+ # the first (and only) key-value pair of the response. The key will vary
232
+ # depending on the original object the photos are related to (e.g 'photos',
233
+ # 'photoset', etc)
234
+ def initialize(photos_api_response={}, api_key=nil)
235
+ photos = photos_api_response.values.first
236
+ [ "page", "pages", "perpage", "total" ].each { |i| instance_variable_set("@#{i}", photos[i])}
237
+ collection = photos['photo'] || []
238
+ collection = [collection] if collection.is_a? Hash
239
+ collection.each { |photo| self << Photo.new(photo.delete('id'), api_key, photo) }
240
+ end
241
+ end
242
+
243
+ # Todo:
244
+ # logged_in?
245
+ # if logged in:
246
+ # flickr.blogs.getList
247
+ # flickr.favorites.add
248
+ # flickr.favorites.remove
249
+ # flickr.groups.browse
250
+ # flickr.photos.getCounts
251
+ # flickr.photos.getNotInSet
252
+ # flickr.photos.getUntagged
253
+ # flickr.photosets.create
254
+ # flickr.photosets.orderSets
255
+ # flickr.test.login
256
+ # uploading
257
+ class User
258
+
259
+ attr_reader :client, :id, :name, :location, :photos_url, :url, :count, :firstdate, :firstdatetaken
260
+
261
+ # A Flickr::User can be instantiated in two ways. The old (deprecated)
262
+ # method is with an ordered series of values. The new method is with a
263
+ # params Hash, which is easier when a variable number of params are
264
+ # supplied, which is the case here, and also avoids having to constantly
265
+ # supply nil values for the email and password, which are now irrelevant
266
+ # as authentication is no longer done this way.
267
+ # An associated flickr client will also be generated if an api key is
268
+ # passed among the arguments or in the params hash. Alternatively, and
269
+ # most likely, an existing client object may be passed in the params hash
270
+ # (e.g. 'client' => some_existing_flickr_client_object), and this is
271
+ # what happends when users are initlialized as the result of a method
272
+ # called on the flickr client (e.g. flickr.users)
273
+ def initialize(id_or_params_hash=nil, username=nil, email=nil, password=nil, api_key=nil)
274
+ if id_or_params_hash.is_a?(Hash)
275
+ id_or_params_hash.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
276
+ else
277
+ @id = id_or_params_hash
278
+ @username = username
279
+ @email = email
280
+ @password = password
281
+ @api_key = api_key
282
+ end
283
+ @client ||= Flickr.new('api_key' => @api_key, 'shared_secret' => @shared_secret, 'auth_token' => @auth_token) if @api_key
284
+ @client.login(@email, @password) if @email and @password # this is now irrelevant as Flickr API no longer supports authentication this way
285
+ end
286
+
287
+ def username
288
+ @username.nil? ? getInfo.username : @username
289
+ end
290
+ def name
291
+ @name.nil? ? getInfo.name : @name
292
+ end
293
+ def location
294
+ @location.nil? ? getInfo.location : @location
295
+ end
296
+ def count
297
+ @count.nil? ? getInfo.count : @count
298
+ end
299
+ def firstdate
300
+ @firstdate.nil? ? getInfo.firstdate : @firstdate
301
+ end
302
+ def firstdatetaken
303
+ @firstdatetaken.nil? ? getInfo.firstdatetaken : @firstdatetaken
304
+ end
305
+
306
+ def photos_url
307
+ @photos_url || getInfo.photos_url
308
+ end
309
+
310
+ # Builds url for user's profile page as per
311
+ # http://www.flickr.com/services/api/misc.urls.html
312
+ def url
313
+ "http://www.flickr.com/people/#{id}/"
314
+ end
315
+
316
+ def pretty_url
317
+ @pretty_url ||= @client.urls_getUserProfile('user_id'=>@id)['user']['url']
318
+ end
319
+
320
+ # Implements flickr.people.getPublicGroups
321
+ def groups
322
+ collection = @client.people_getPublicGroups('user_id'=>@id)['groups']['group']
323
+ collection = [collection] if collection.is_a? Hash
324
+ collection.collect { |group| Group.new( "id" => group['nsid'],
325
+ "name" => group['name'],
326
+ "eighteenplus" => group['eighteenplus'],
327
+ "client" => @client) }
328
+ end
329
+
330
+ # Implements flickr.people.getPublicPhotos. Options hash allows you to add
331
+ # extra restrictions as per flickr.people.getPublicPhotos docs, e.g.
332
+ # user.photos('per_page' => '25', 'extras' => 'date_taken')
333
+ def photos(options={})
334
+ @client.photos_request('people.getPublicPhotos', {'user_id' => @id}.merge(options))
335
+ # what about non-public photos?
336
+ end
337
+
338
+ # Gets photos with a given tag
339
+ def tag(tag)
340
+ @client.photos('user_id'=>@id, 'tags'=>tag)
341
+ end
342
+
343
+ # Implements flickr.contacts.getPublicList and flickr.contacts.getList
344
+ def contacts
345
+ @client.contacts_getPublicList('user_id'=>@id)['contacts']['contact'].collect { |contact| User.new(contact['nsid'], contact['username'], nil, nil, @api_key) }
346
+ #or
347
+ end
348
+
349
+ # Implements flickr.favorites.getPublicList
350
+ def favorites
351
+ @client.photos_request('favorites.getPublicList', 'user_id' => @id)
352
+ end
353
+
354
+ # Implements flickr.photosets.getList
355
+ def photosets
356
+ @client.photosets_getList('user_id'=>@id)['photosets']['photoset'].collect { |photoset| Photoset.new(photoset['id'], @api_key) }
357
+ end
358
+
359
+ # Implements flickr.tags.getListUser
360
+ def tags
361
+ @client.tags_getListUser('user_id'=>@id)['who']['tags']['tag'].collect { |tag| tag }
362
+ end
363
+
364
+ # Implements flickr.tags.getListUserPopular
365
+ def popular_tags(count = 10)
366
+ @client.tags_getListUserPopular('user_id'=>@id, 'count'=> count)['who']['tags']['tag'].each { |tag_score| tag_score["tag"] = tag_score.delete("content") }
367
+ end
368
+
369
+ # Implements flickr.photos.getContactsPublicPhotos and flickr.photos.getContactsPhotos
370
+ def contactsPhotos
371
+ @client.photos_request('photos.getContactsPublicPhotos', 'user_id' => @id)
372
+ end
373
+
374
+ def to_s
375
+ @name
376
+ end
377
+
378
+ private
379
+
380
+ # Implements flickr.people.getInfo, flickr.urls.getUserPhotos, and flickr.urls.getUserProfile
381
+ def getInfo
382
+ unless @info
383
+ @info = @client.people_getInfo('user_id'=>@id)['person']
384
+ @username = @info['username']
385
+ @name = @info['realname']
386
+ @location = @info['location']
387
+ @photos_url = @info['photosurl']
388
+ @count = @info['photos']['count']
389
+ @firstdate = @info['photos']['firstdate']
390
+ @firstdatetaken = @info['photos']['firstdatetaken']
391
+ end
392
+ self
393
+ end
394
+
395
+ end
396
+
397
+ class Photo
398
+
399
+ attr_reader :id, :client, :title
400
+
401
+ def initialize(id=nil, api_key=nil, extra_params={})
402
+ @id = id
403
+ @api_key = api_key
404
+ extra_params.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
405
+ @client = Flickr.new @api_key
406
+ end
407
+
408
+ # Allows access to all photos instance variables through hash like
409
+ # interface, e.g. photo["datetaken"] returns @datetaken instance
410
+ # variable. Useful for accessing any weird and wonderful parameter
411
+ # that may have been returned by Flickr when finding the photo,
412
+ # e.g. those returned by the extras argument in
413
+ # flickr.people.getPublicPhotos
414
+ def [](param_name)
415
+ instance_variable_get("@#{param_name}")
416
+ end
417
+
418
+ def title
419
+ @title.nil? ? getInfo("title") : @title
420
+ end
421
+
422
+ # Returns the owner of the photo as a Flickr::User. If we have no info
423
+ # about the owner, we make an API call to get it. If we already have
424
+ # the owner's id, create a user based on that. Either way, we cache the
425
+ # result so we don't need to check again
426
+ def owner
427
+ case @owner
428
+ when Flickr::User
429
+ @owner
430
+ when String
431
+ @owner = Flickr::User.new(@owner, nil, nil, nil, @api_key)
432
+ else
433
+ getInfo("owner")
434
+ end
435
+ end
436
+
437
+ def server
438
+ @server.nil? ? getInfo("server") : @server
439
+ end
440
+
441
+ def isfavorite
442
+ @isfavorite.nil? ? getInfo("isfavorite") : @isfavorite
443
+ end
444
+
445
+ def license
446
+ @license.nil? ? getInfo("license") : @license
447
+ end
448
+
449
+ def rotation
450
+ @rotation.nil? ? getInfo("rotation") : @rotation
451
+ end
452
+
453
+ def description
454
+ @description || getInfo("description")
455
+ end
456
+
457
+ def notes
458
+ @notes.nil? ? getInfo("notes") : @notes
459
+ end
460
+
461
+ # Returns the URL for the photo size page
462
+ # defaults to 'Medium'
463
+ # other valid sizes are in the VALID_SIZES hash
464
+ def size_url(size='Medium')
465
+ uri_for_photo_from_self(size) || sizes(size)['url']
466
+ end
467
+
468
+ # converts string or symbol size to a capitalized string
469
+ def normalize_size(size)
470
+ size ? size.to_s.capitalize : size
471
+ end
472
+
473
+ # the URL for the main photo page
474
+ # if getInfo has already been called, this will return the pretty url
475
+ #
476
+ # for historical reasons, an optional size can be given
477
+ # 'Medium' returns the regular url; any other size returns a size page
478
+ # use size_url instead
479
+ def url(size = nil)
480
+ if normalize_size(size) != 'Medium'
481
+ size_url(size)
482
+ else
483
+ @url || uri_for_photo_from_self
484
+ end
485
+ end
486
+
487
+ # the 'pretty' url for a photo
488
+ # (if the user has set up a custom name)
489
+ # eg, http://flickr.com/photos/granth/2584402507/ instead of
490
+ # http://flickr.com/photos/23386158@N00/2584402507/
491
+ def pretty_url
492
+ @url || getInfo("pretty_url")
493
+ end
494
+
495
+ # Returns the URL for the image (default or any specified size)
496
+ def source(size='Medium')
497
+ image_source_uri_from_self(size) || sizes(size)['source']
498
+ end
499
+
500
+ # Returns the photo file data itself, in any specified size. Example: File.open(photo.title, 'w') { |f| f.puts photo.file }
501
+ def file(size='Medium')
502
+ Net::HTTP.get_response(URI.parse(source(size))).body
503
+ end
504
+
505
+ # Unique filename for the image, based on the Flickr NSID
506
+ def filename
507
+ "#{@id}.jpg"
508
+ end
509
+
510
+ # Implements flickr.photos.getContext
511
+ def context
512
+ context = @client.photos_getContext('photo_id'=>@id)
513
+ @previousPhoto = Photo.new(context['prevphoto'].delete('id'), @api_key, context['prevphoto']) if context['prevphoto']['id']!='0'
514
+ @nextPhoto = Photo.new(context['nextphoto'].delete('id'), @api_key, context['nextphoto']) if context['nextphoto']['id']!='0'
515
+ return [@previousPhoto, @nextPhoto]
516
+ end
517
+
518
+ # Implements flickr.photos.getExif
519
+ def exif
520
+ @client.photos_getExif('photo_id'=>@id)['photo']
521
+ end
522
+
523
+ # Implements flickr.photos.getPerms
524
+ def permissions
525
+ @client.photos_getPerms('photo_id'=>@id)['perms']
526
+ end
527
+
528
+ # Implements flickr.photos.getSizes
529
+ def sizes(size=nil)
530
+ size = normalize_size(size)
531
+ sizes = @client.photos_getSizes('photo_id'=>@id)['sizes']['size']
532
+ sizes = sizes.find{|asize| asize['label']==size} if size
533
+ return sizes
534
+ end
535
+
536
+ # flickr.tags.getListPhoto
537
+ def tags
538
+ @client.tags_getListPhoto('photo_id'=>@id)['photo']['tags']
539
+ end
540
+
541
+ # Implements flickr.photos.notes.add
542
+ def add_note(note)
543
+ end
544
+
545
+ # Implements flickr.photos.setDates
546
+ def dates=(dates)
547
+ end
548
+
549
+ # Implements flickr.photos.setPerms
550
+ def perms=(perms)
551
+ end
552
+
553
+ # Implements flickr.photos.setTags
554
+ def tags=(tags)
555
+ end
556
+
557
+ # Implements flickr.photos.setMeta
558
+ def title=(title)
559
+ end
560
+ def description=(title)
561
+ end
562
+
563
+ # Implements flickr.photos.addTags
564
+ def add_tag(tag)
565
+ end
566
+
567
+ # Implements flickr.photos.removeTag
568
+ def remove_tag(tag)
569
+ end
570
+
571
+ # Implements flickr.photos.transform.rotate
572
+ def rotate
573
+ end
574
+
575
+ # Implements flickr.blogs.postPhoto
576
+ def postToBlog(blog_id, title='', description='')
577
+ @client.blogs_postPhoto('photo_id'=>@id, 'title'=>title, 'description'=>description)
578
+ end
579
+
580
+ # Implements flickr.photos.notes.delete
581
+ def deleteNote(note_id)
582
+ end
583
+
584
+ # Implements flickr.photos.notes.edit
585
+ def editNote(note_id)
586
+ end
587
+
588
+ # Converts the Photo to a string by returning its title
589
+ def to_s
590
+ title
591
+ end
592
+
593
+ private
594
+
595
+ # Implements flickr.photos.getInfo
596
+ def getInfo(attrib="")
597
+ return instance_variable_get("@#{attrib}") if @got_info
598
+ info = @client.photos_getInfo('photo_id'=>@id)['photo']
599
+ @got_info = true
600
+ info.each { |k,v| instance_variable_set("@#{k}", v)}
601
+ @owner = User.new(info['owner']['nsid'], info['owner']['username'], nil, nil, @api_key)
602
+ @tags = info['tags']['tag']
603
+ @notes = info['notes']['note']#.collect { |note| Note.new(note.id) }
604
+ @url = info['urls']['url']['content'] # assumes only one url
605
+ instance_variable_get("@#{attrib}")
606
+ end
607
+
608
+ # Builds source uri of image from params (often returned from other
609
+ # methods, e.g. User#photos). As specified at:
610
+ # http://www.flickr.com/services/api/misc.urls.html. If size is given
611
+ # should be one the keys in the VALID_SIZES hash, i.e.
612
+ # "Square", "Thumbnail", "Medium", "Large", "Original", "Small" (These
613
+ # are the values returned by flickr.photos.getSizes).
614
+ # If no size is given the uri for "Medium"-size image, i.e. with width
615
+ # of 500 is returned
616
+ # TODO: Handle "Original" size
617
+ def image_source_uri_from_self(size=nil)
618
+ return unless @farm&&@server&&@id&&@secret
619
+ s_size = VALID_SIZES[normalize_size(size)] # get the short letters array corresponding to the size
620
+ s_size = s_size&&s_size[0] # the first element of this array is used to build the source uri
621
+ if s_size.nil?
622
+ "http://farm#{@farm}.static.flickr.com/#{@server}/#{@id}_#{@secret}.jpg"
623
+ else
624
+ "http://farm#{@farm}.static.flickr.com/#{@server}/#{@id}_#{@secret}_#{s_size}.jpg"
625
+ end
626
+ end
627
+
628
+ # Builds uri of Flickr page for photo. By default returns the main
629
+ # page for the photo, but if passed a size will return the simplified
630
+ # flickr page featuring the given size of the photo
631
+ # TODO: Handle "Original" size
632
+ def uri_for_photo_from_self(size=nil)
633
+ return unless @owner&&@id
634
+ size = normalize_size(size)
635
+ s_size = VALID_SIZES[size] # get the short letters array corresponding to the size
636
+ 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
637
+ "http://www.flickr.com/photos/#{owner.id}/#{@id}" + (s_size ? "/sizes/#{s_size}/" : "")
638
+ end
639
+ end
640
+
641
+ # Todo:
642
+ # flickr.groups.pools.add
643
+ # flickr.groups.pools.getContext
644
+ # flickr.groups.pools.getGroups
645
+ # flickr.groups.pools.getPhotos
646
+ # flickr.groups.pools.remove
647
+ class Group
648
+ attr_reader :id, :client, :description, :name, :eighteenplus, :members, :online, :privacy, :url#, :chatid, :chatcount
649
+
650
+ def initialize(id_or_params_hash=nil, api_key=nil)
651
+ if id_or_params_hash.is_a?(Hash)
652
+ id_or_params_hash.each { |k,v| self.instance_variable_set("@#{k}", v) } # convert extra_params into instance variables
653
+ else
654
+ @id = id_or_params_hash
655
+ @api_key = api_key
656
+ @client = Flickr.new @api_key
657
+ end
658
+ end
659
+
660
+ # Implements flickr.groups.getInfo and flickr.urls.getGroup
661
+ # private, once we can call it as needed
662
+ def getInfo
663
+ info = @client.groups_getInfo('group_id'=>@id)['group']
664
+ @name = info['name']
665
+ @members = info['members']
666
+ @online = info['online']
667
+ @privacy = info['privacy']
668
+ # @chatid = info['chatid']
669
+ # @chatcount = info['chatcount']
670
+ @url = @client.urls_getGroup('group_id'=>@id)['group']['url']
671
+ self
672
+ end
673
+
674
+ end
675
+
676
+ # Todo:
677
+ # flickr.photosets.delete
678
+ # flickr.photosets.editMeta
679
+ # flickr.photosets.editPhotos
680
+ # flickr.photosets.getContext
681
+ # flickr.photosets.getInfo
682
+ # flickr.photosets.getPhotos
683
+ class Photoset
684
+
685
+ attr_reader :id, :client, :owner, :primary, :photos, :title, :description, :url
686
+
687
+ def initialize(id=nil, api_key=nil)
688
+ @id = id
689
+ @api_key = api_key
690
+ @client = Flickr.new @api_key
691
+ end
692
+
693
+ def owner
694
+ @owner || getInfo.owner
695
+ end
696
+
697
+ def primary
698
+ @primary || getInfo.primary
699
+ end
700
+
701
+ def title
702
+ @title || getInfo.title
703
+ end
704
+
705
+ def url
706
+ @url || getInfo.url
707
+ end
708
+
709
+ def photos
710
+ @photos ||= getPhotos
711
+ end
712
+
713
+ def first_photo
714
+ @first_photo ||= getFirstPhoto
715
+ end
716
+
717
+ private
718
+ def getInfo
719
+ unless @info
720
+ @info = @client.photosets_getInfo('photoset_id'=>@id)['photoset']
721
+ @owner = User.new(@info['owner'], nil, nil, nil, @api_key)
722
+ @primary = @info['primary']
723
+ @title = @info['title']
724
+ @description = @info['description']
725
+ @url = "#{@owner.photos_url}sets/#{@id}/"
726
+ end
727
+ self
728
+ end
729
+
730
+ def getPhotos
731
+ @client.photos_request('photosets.getPhotos', {'photoset_id' => @id})
732
+ end
733
+
734
+ def getFirstPhoto
735
+ @client.photos_request('photosets.getPhotos', {'photoset_id' => @id, :per_page => 1}).first
736
+ end
737
+ end
738
+
739
+ end