ruby-picasa 0.2.0

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.
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
+