ruby-picasa 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.autotest ADDED
@@ -0,0 +1,7 @@
1
+ require 'autotest'
2
+ require 'autotest/rspec'
3
+
4
+ Autotest.add_hook :initialize do |at|
5
+ at.add_exception %r{^\./(?:\.git|pkg|doc)}
6
+ at.add_exception %r{\.sw[op]$}
7
+ end
data/History.txt ADDED
@@ -0,0 +1,11 @@
1
+ === 0.2.0 / 2009-02-25
2
+
3
+ * First public release.
4
+
5
+ === 0.1.0 / 2009-02-21
6
+
7
+ * First working version.
8
+
9
+ === 0.0.0 / 2009-02-21
10
+
11
+ * Needed to access Picasa.
data/Manifest.txt ADDED
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/ruby_picasa/types.rb
6
+ lib/ruby_picasa.rb
7
+ spec/ruby_picasa/types_spec.rb
8
+ spec/ruby_picasa_spec.rb
9
+ spec/sample/album.atom
10
+ spec/sample/recent.atom
11
+ spec/sample/search.atom
12
+ spec/sample/user.atom
13
+ spec/spec.opts
14
+ spec/spec_helper.rb
15
+ .autotest
data/README.txt ADDED
@@ -0,0 +1,74 @@
1
+ = ruby_picasa
2
+
3
+ * http://github.com/pangloss/ruby_picasa
4
+
5
+ == DESCRIPTION:
6
+
7
+ Provides a super easy to use object layer for authenticating and accessing
8
+ Picasa through their API.
9
+
10
+ == FEATURES:
11
+
12
+ * Simplifies the process of obtaining both a temporary and a permanent AuthSub
13
+ token.
14
+ * Very easy to use API.
15
+ * Allows access to both public and private User, Album and Photo data.
16
+ * Uses Objectify::Xml to define the XML object-relational layer with a very
17
+ easy to understand DSL. See www.github.com/pangloss/objectify_xml
18
+
19
+ == PROBLEMS:
20
+
21
+ * None known.
22
+
23
+ == SYNOPSIS:
24
+
25
+ # 1. Authorize application for access (in a rails controller)
26
+ #
27
+ redirect_to RubyPicasa.authorization_url(auth_result_url)
28
+
29
+ # 2. Extract the Picasa token from the request Picasa sends back to your app
30
+ # and create a permanent AuthSub token. Returns an initialized Picasa
31
+ # session. (Called from the Rails action for auth_result_url above)
32
+ picasa = RubyPicasa.authorize_request(self.request)
33
+
34
+ # 3. Access the data you are interested in
35
+ @album = picasa.user.albums.first
36
+ @photo = @album.photos.first
37
+
38
+ # 4. Display your photos
39
+ image_tag @photo.url
40
+ image_tag @photo.url('160c') # Picasa thumbnail names are predefined
41
+
42
+ == REQUIREMENTS:
43
+
44
+ * objectify_xml
45
+
46
+ == INSTALL:
47
+
48
+ * gem install ruby-picasa
49
+ * gem install pangloss-ruby-picasa --source http://gems.github.com
50
+
51
+ == LICENSE:
52
+
53
+ (The MIT License)
54
+
55
+ Copyright (c) 2009 Darrick Wiebe
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/Rakefile ADDED
@@ -0,0 +1,23 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/ruby_picasa.rb'
6
+ require 'spec/rake/spectask'
7
+
8
+ Hoe.new('ruby-picasa', RubyPicasa::VERSION) do |p|
9
+ p.rubyforge_name = 'ruby-picasa'
10
+ p.developer('pangloss', 'darrick@innatesoftware.com')
11
+ p.extra_deps = 'objectify-xml'
12
+ p.testlib = 'spec'
13
+ p.test_globs = 'spec/**/*_spec.rb'
14
+ p.remote_rdoc_dir = ''
15
+ end
16
+
17
+ desc "Run all specifications"
18
+ Spec::Rake::SpecTask.new(:spec) do |t|
19
+ t.libs = ['lib', 'spec']
20
+ t.spec_opts = ['--colour', '--format', 'specdoc']
21
+ end
22
+
23
+ # vim: syntax=Ruby
@@ -0,0 +1,279 @@
1
+ require 'objectify_xml'
2
+ require 'objectify_xml/atom'
3
+ require 'cgi'
4
+ require 'net/http'
5
+ require 'net/https'
6
+ require File.join(File.dirname(__FILE__), 'ruby_picasa/types')
7
+
8
+ module RubyPicasa
9
+ VERSION = '0.2.0'
10
+
11
+ class PicasaError < StandardError
12
+ end
13
+
14
+ class PicasaTokenError < PicasaError
15
+ end
16
+ end
17
+
18
+ class Picasa
19
+ include RubyPicasa
20
+
21
+ class << self
22
+ # The user must be redirected to this address to authorize the application
23
+ # to access their Picasa account. The token_from_request and
24
+ # authorize_request methods can be used to handle the resulting redirect
25
+ # from Picasa.
26
+ def authorization_url(return_to_url, request_session = true, secure = false)
27
+ session = request_session ? '1' : '0'
28
+ secure = secure ? '1' : '0'
29
+ return_to_url = CGI.escape(return_to_url)
30
+ url = 'http://www.google.com/accounts/AuthSubRequest'
31
+ "#{ url }?scope=http%3A%2F%2Fpicasaweb.google.com%2Fdata%2F&session=#{ session }&secure=#{ secure }&next=#{ return_to_url }"
32
+ end
33
+
34
+ # Takes a Rails request object and extracts the token from it. This would
35
+ # happen in the action that is pointed to by the return_to_url argument
36
+ # when the authorization_url is created.
37
+ def token_from_request(request)
38
+ if token = request.params['token']
39
+ return token
40
+ else
41
+ raise PicasaTokenError, 'No Picasa authorization token was found.'
42
+ end
43
+ end
44
+
45
+ # Takes a Rails request object as in token_from_request, then makes the
46
+ # token authorization request to produce the permanent token. This will
47
+ # only work if request_session was true when you created the
48
+ # authorization_url.
49
+ def authorize_request(request)
50
+ p = Picasa.new(token_from_request(request))
51
+ p.authorize_token!
52
+ p
53
+ end
54
+
55
+ def host
56
+ 'picasaweb.google.com'
57
+ end
58
+
59
+ def is_url?(path)
60
+ path.to_s =~ %r{\Ahttps?://}
61
+ end
62
+
63
+ # For more on possible options and their meanings, see:
64
+ # http://code.google.com/apis/picasaweb/reference.html
65
+ #
66
+ # The following values are valid for the thumbsize and imgmax query
67
+ # parameters and are embeddable on a webpage. These images are available as
68
+ # both cropped(c) and uncropped(u) sizes by appending c or u to the size.
69
+ # As an example, to retrieve a 72 pixel image that is cropped, you would
70
+ # specify 72c, while to retrieve the uncropped image, you would specify 72u
71
+ # for the thumbsize or imgmax query parameter values.
72
+ #
73
+ # 32, 48, 64, 72, 144, 160
74
+ #
75
+ # The following values are valid for the thumbsize and imgmax query
76
+ # parameters and are embeddable on a webpage. These images are available as
77
+ # only uncropped(u) sizes by appending u to the size or just passing the
78
+ # size value without appending anything.
79
+ #
80
+ # 200, 288, 320, 400, 512, 576, 640, 720, 800
81
+ #
82
+ # The following values are valid for the thumbsize and imgmax query
83
+ # parameters and are not embeddable on a webpage. These image sizes are
84
+ # only available in uncropped format and are accessed using only the size
85
+ # (no u is appended to the size).
86
+ #
87
+ # 912, 1024, 1152, 1280, 1440, 1600
88
+ #
89
+ def path(args = {})
90
+ path, options = parse_url(args)
91
+ if path.nil?
92
+ path = ["/data/feed/api"]
93
+ if args[:user_id] == 'all'
94
+ path += ["all"]
95
+ else
96
+ path += ["user", CGI.escape(args[:user_id] || 'default')]
97
+ end
98
+ path += ['albumid', CGI.escape(args[:album_id])] if args[:album_id]
99
+ path = path.join('/')
100
+ end
101
+ options['kind'] = 'photo' if args[:recent_photos] or args[:album_id]
102
+ [:max_results, :start_index, :tag, :q, :kind,
103
+ :access, :thumbsize, :imgmax, :bbox, :l].each do |arg|
104
+ options[arg.to_s.dasherize] = args[arg] if args[arg]
105
+ end
106
+ if options.empty?
107
+ path
108
+ else
109
+ [path, options.map { |k, v| [k.to_s, CGI.escape(v.to_s)].join('=') }.join('&')].join('?')
110
+ end
111
+ end
112
+
113
+ private
114
+
115
+ def parse_url(args)
116
+ url = args[:url]
117
+ url ||= args[:user_id] if is_url?(args[:user_id])
118
+ url ||= args[:album_id] if is_url?(args[:album_id])
119
+ if url
120
+ uri = URI.parse(url)
121
+ path = uri.path
122
+ options = {}
123
+ if uri.query
124
+ uri.query.split('&').each do |query|
125
+ k, v = query.split('=')
126
+ options[k] = CGI.unescape(v)
127
+ end
128
+ end
129
+ [path, options]
130
+ else
131
+ [nil, {}]
132
+ end
133
+ end
134
+ end
135
+
136
+ attr_reader :token
137
+
138
+ def initialize(token)
139
+ @token = token
140
+ @request_cache = {}
141
+ end
142
+
143
+ def authorize_token!
144
+ http = Net::HTTP.new("www.google.com", 443)
145
+ http.use_ssl = true
146
+ response = http.get('/accounts/accounts/AuthSubSessionToken', auth_header)
147
+ token = response.body.scan(/Token=(.*)/).flatten.compact.first
148
+ if token
149
+ @token = token
150
+ else
151
+ raise RubyPicasa::PicasaTokenError, 'The request to upgrade to a session token failed.'
152
+ end
153
+ @token
154
+ end
155
+
156
+ def user(user_id_or_url = 'default', options = {})
157
+ get(options.merge(:user_id => user_id_or_url))
158
+ end
159
+
160
+ def album(album_id_or_url, options = {})
161
+ get(options.merge(:album_id => album_id_or_url))
162
+ end
163
+
164
+ # This request does not require authentication.
165
+ def search(q, options = {})
166
+ h = {}
167
+ h[:max_results] = 10
168
+ h[:user_id] = 'all'
169
+ h[:kind] = 'photo'
170
+ # merge options over h, but merge q over options
171
+ get(h.merge(options).merge(:q => q))
172
+ end
173
+
174
+ def get_url(url, options = {})
175
+ get(options.merge(:url => url))
176
+ end
177
+
178
+ def recent_photos(user_id_or_url = 'default', options = {})
179
+ if user_id_or_url.is_a?(Hash)
180
+ options = user_id_or_url
181
+ user_id_or_url = 'default'
182
+ end
183
+ h = {}
184
+ h[:user_id] = user_id_or_url
185
+ h[:recent_photos] = true
186
+ get(options.merge(h))
187
+ end
188
+
189
+ def album_by_title(title, options = {})
190
+ if a = user.albums.find { |a| title === a.title }
191
+ a.load options
192
+ end
193
+ end
194
+
195
+ def xml(options = {})
196
+ http = Net::HTTP.new(Picasa.host, 80)
197
+ path = Picasa.path(options)
198
+ response = http.get(path, auth_header)
199
+ if response.code =~ /20[01]/
200
+ response.body
201
+ end
202
+ end
203
+
204
+ def get(options = {})
205
+ with_cache(options) do |xml|
206
+ class_from_xml(xml)
207
+ end
208
+ end
209
+
210
+ private
211
+
212
+ def auth_header
213
+ if token
214
+ { "Authorization" => %{AuthSub token="#{ token }"} }
215
+ else
216
+ {}
217
+ end
218
+ end
219
+
220
+ def with_cache(options)
221
+ path = Picasa.path(options)
222
+ @request_cache.delete(path) if options[:reload]
223
+ xml = nil
224
+ if @request_cache.has_key? path
225
+ xml = @request_cache[path]
226
+ else
227
+ xml = @request_cache[path] = xml(options)
228
+ end
229
+ if xml
230
+ yield xml
231
+ end
232
+ end
233
+
234
+ def xml_data(xml)
235
+ if xml = Objectify::Xml.first_element(xml)
236
+ # There is something wrong with Nokogiri xpath/css search with
237
+ # namespaces. If you are searching a document that has namespaces,
238
+ # it's impossible to match any elements in the root xmlns namespace.
239
+ # Matching just on attributes works though.
240
+ feed, entry = xml.search('//*[@term][@scheme]', xml.namespaces)
241
+ feed_scheme = feed['term'] if feed
242
+ entry_scheme = entry['term'] if entry
243
+ [xml, feed_scheme, entry_scheme]
244
+ end
245
+ end
246
+
247
+ def class_from_xml(xml)
248
+ xml, feed_scheme, entry_scheme = xml_data(xml)
249
+ if xml
250
+ r = case feed_scheme
251
+ when /#user$/
252
+ case entry_scheme
253
+ when /#album$/
254
+ User.new(xml, self)
255
+ when /#photo$/
256
+ RecentPhotos.new(xml, self)
257
+ end
258
+ when /#album$/
259
+ case entry_scheme
260
+ when nil, /#photo$/
261
+ Album.new(xml, self)
262
+ end
263
+ when /#photo$/
264
+ case entry_scheme
265
+ when /#photo$/
266
+ Search.new(xml, self)
267
+ when nil
268
+ Photo.new(xml, self)
269
+ end
270
+ end
271
+ if r
272
+ r.session = self
273
+ r
274
+ else
275
+ raise PicasaError, "Unknown feed type\n feed: #{ feed_scheme }\n entry: #{ entry_scheme }"
276
+ end
277
+ end
278
+ end
279
+ end
@@ -0,0 +1,165 @@
1
+ # Note that in all defined classes I'm ignoring values I don't happen to care
2
+ # about. If you care about them, please feel free to add support for them,
3
+ # which should not be difficult.
4
+ #
5
+ # Plural attribute names will be treated as arrays unless the element name
6
+ # in the xml document is already plural. (Convention seems to be to label
7
+ # repeating elements in the singular.)
8
+ #
9
+ # If an attribute should be a non-trivial datatype, define the mapping from
10
+ # the fully namespaced attribute name to the class you wish to use in the
11
+ # class method #types.
12
+ #
13
+ # Define which namespaces you support in the class method #namespaces. Any
14
+ # elements defined in other namespaces are automatically ignored.
15
+ module RubyPicasa
16
+ class PhotoUrl < Objectify::ElementParser
17
+ attributes :url, :height, :width
18
+ end
19
+
20
+
21
+ class ThumbnailUrl < PhotoUrl
22
+ def thumb_name
23
+ url.scan(%r{/([^/]+)/[^/]+$}).flatten.compact.first
24
+ end
25
+ end
26
+
27
+
28
+ class Base < Objectify::DocumentParser
29
+ namespaces :openSearch, :gphoto, :media
30
+ flatten 'media:group'
31
+
32
+ attribute :id, 'id'
33
+ attributes :updated, :title
34
+
35
+ has_many :links, Objectify::Atom::Link, 'link'
36
+ has_one :content, PhotoUrl, 'media:content'
37
+ has_many :thumbnails, ThumbnailUrl, 'media:thumbnail'
38
+ has_one :author, Objectify::Atom::Author, 'author'
39
+
40
+ def link(rel)
41
+ links.find { |l| l.rel == rel }
42
+ end
43
+
44
+ def session=(session)
45
+ @session = session
46
+ end
47
+
48
+ def session
49
+ if @session
50
+ @session
51
+ else
52
+ @session = parent.session if parent
53
+ end
54
+ end
55
+
56
+ def load(options = {})
57
+ session.get_url(id, options)
58
+ end
59
+
60
+ def next
61
+ if link = link('next')
62
+ session.get_url(link.href)
63
+ end
64
+ end
65
+
66
+ def previous
67
+ if link = link('previous')
68
+ session.get_url(link.href)
69
+ end
70
+ end
71
+ end
72
+
73
+
74
+ class User < Base
75
+ attributes :total_results, # represents total number of albums
76
+ :start_index,
77
+ :items_per_page,
78
+ :thumbnail
79
+ has_many :entries, :Album, 'entry'
80
+
81
+ def albums
82
+ entries
83
+ end
84
+ end
85
+
86
+
87
+ class RecentPhotos < User
88
+ has_many :entries, :Photo, 'entry'
89
+
90
+ def photos
91
+ entries
92
+ end
93
+
94
+ undef albums
95
+ end
96
+
97
+
98
+ class Album < Base
99
+ attributes :published,
100
+ :summary,
101
+ :rights,
102
+ :gphoto_id,
103
+ :name,
104
+ :access,
105
+ :numphotos, # number of pictures in this album
106
+ :total_results, # number of pictures matching this 'search'
107
+ :start_index,
108
+ :items_per_page,
109
+ :allow_downloads
110
+ has_many :entries, :Photo, 'entry'
111
+
112
+ def public?
113
+ rights == 'public'
114
+ end
115
+
116
+ def private?
117
+ rights == 'private'
118
+ end
119
+
120
+ def photos(options = {})
121
+ if entries.blank? and !@photos_requested
122
+ @photos_requested = true
123
+ self.session ||= parent.session
124
+ self.entries = session.album(id, options).entries if self.session
125
+ else
126
+ entries
127
+ end
128
+ end
129
+ end
130
+
131
+
132
+ class Search < Album
133
+ end
134
+
135
+
136
+ class Photo < Base
137
+ attributes :published,
138
+ :summary,
139
+ :gphoto_id,
140
+ :version, # can use to determine if need to update...
141
+ :position,
142
+ :albumid, # useful from the recently updated feed for instance.
143
+ :width,
144
+ :height,
145
+ :description,
146
+ :keywords,
147
+ :credit
148
+ has_one :author, Objectify::Atom::Author, 'author'
149
+
150
+ def url(thumb_name = nil)
151
+ if thumb_name
152
+ if thumb = thumbnail(thumb_name)
153
+ thumb.url
154
+ end
155
+ else
156
+ content.url
157
+ end
158
+ end
159
+
160
+ def thumbnail(thumb_name)
161
+ thumbnails.find { |t| t.thumb_name == thumb_name }
162
+ end
163
+ end
164
+ end
165
+