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 +7 -0
- data/History.txt +11 -0
- data/Manifest.txt +15 -0
- data/README.txt +74 -0
- data/Rakefile +23 -0
- data/lib/ruby_picasa.rb +279 -0
- data/lib/ruby_picasa/types.rb +165 -0
- data/spec/ruby_picasa/types_spec.rb +221 -0
- data/spec/ruby_picasa_spec.rb +319 -0
- data/spec/sample/album.atom +141 -0
- data/spec/sample/recent.atom +111 -0
- data/spec/sample/search.atom +99 -0
- data/spec/sample/user.atom +107 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +13 -0
- metadata +91 -0
data/.autotest
ADDED
data/History.txt
ADDED
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
|
data/lib/ruby_picasa.rb
ADDED
@@ -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
|
+
|