smugmug 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/HISTORY +2 -0
  2. data/LICENSE +19 -0
  3. data/MANIFEST +48 -0
  4. data/README +30 -0
  5. data/Rakefile +100 -0
  6. data/bin/smcli +225 -0
  7. data/bin/smugmug2sql +158 -0
  8. data/doc/API +310 -0
  9. data/doc/TODO +32 -0
  10. data/lib/net/httpz.rb +31 -0
  11. data/lib/smugmug.rb +179 -0
  12. data/lib/smugmug/album/info.rb +131 -0
  13. data/lib/smugmug/album/stats.rb +31 -0
  14. data/lib/smugmug/albums.rb +39 -0
  15. data/lib/smugmug/base.rb +104 -0
  16. data/lib/smugmug/cache.rb +33 -0
  17. data/lib/smugmug/config.rb +48 -0
  18. data/lib/smugmug/image/exif.rb +72 -0
  19. data/lib/smugmug/image/info.rb +88 -0
  20. data/lib/smugmug/image/stats.rb +32 -0
  21. data/lib/smugmug/images.rb +52 -0
  22. data/lib/smugmug/table.rb +133 -0
  23. data/lib/smugmug/util.rb +12 -0
  24. data/test/album.rb +359 -0
  25. data/test/config.rb +39 -0
  26. data/test/httpz.rb +120 -0
  27. data/test/image.rb +540 -0
  28. data/test/login.rb +24 -0
  29. data/test/runner.rb +83 -0
  30. data/test/servlet.rb +257 -0
  31. data/test/table.rb +113 -0
  32. data/xml/canned +212 -0
  33. data/xml/fail/empty.set.xml +4 -0
  34. data/xml/fail/invalid.apikey.xml +4 -0
  35. data/xml/fail/invalid.login.xml +4 -0
  36. data/xml/fail/invalid.method.xml +4 -0
  37. data/xml/fail/invalid.user.xml +4 -0
  38. data/xml/fail/system.error.xml +4 -0
  39. data/xml/standard/albums.get.xml +24 -0
  40. data/xml/standard/albums.getInfo.xml +38 -0
  41. data/xml/standard/albums.getStats.xml +43 -0
  42. data/xml/standard/categories.get.xml +213 -0
  43. data/xml/standard/images.get.xml +9 -0
  44. data/xml/standard/images.getEXIF.xml +34 -0
  45. data/xml/standard/images.getInfo.xml +29 -0
  46. data/xml/standard/images.getStats.xml +15 -0
  47. data/xml/standard/login.withHash.xml +7 -0
  48. data/xml/standard/login.withPassword.xml +10 -0
  49. metadata +103 -0
data/doc/API ADDED
@@ -0,0 +1,310 @@
1
+ # -*- text -*-
2
+ # $Hg$
3
+
4
+ = Introduction
5
+
6
+ The majority of the SmugMug's function are auto-generated. This means rdoc
7
+ documentation is useless because none of the functions are visible. It is
8
+ probably still preferable to document the function within class files, but for
9
+ now I am going to document them here. This way everything is in one location.
10
+
11
+ == Conventions
12
+
13
+ * SmugMug refers to the company, or web site.
14
+ * smugmug refers to this library.
15
+
16
+ === Method
17
+ ==== Naming
18
+
19
+ SmugMug and ruby do not share the same naming convetions. For example, a
20
+ function to return an album's ID is called AlbumID by Smugmug, but ruby would
21
+ name it album_id. To keep both factions happy smugmug provides both naming
22
+ conventions. When data are boolean types the ruby method names have a trailing
23
+ question mark. This convention is used throughout ruby.
24
+
25
+ ==== Return Types
26
+
27
+ There are four data types that are returned by SmugMug. They are boolean, int,
28
+ string, and float. The smugmug library ensures that all data returned is cast
29
+ to the appropriate type. For example, the data returned from smugmug.com for
30
+ AlbumID is a string, but smugmug returns a Fixnum (integer). This convention
31
+ is broken with dealing with EXIF data. Camera manufacturers may encode unit
32
+ information in their data, thereby preventing the casting of data. For
33
+ example, the method focal length for my Canon SD500 return 7mm.
34
+
35
+ The type information for the methods can be found on SmugMug's wiki at
36
+ http://smugmug.jot.com/WikiHome/API/Versions/1.1.1.
37
+
38
+ = SmugMug::SmugMug
39
+
40
+ The first time you use the library you need to login in SmugMug. This requires
41
+ a username and password, all subsequent login attempts do not require a
42
+ username, and password. The first time you login SmugMug returns your UserID,
43
+ and a password hash. These data are stored in ~/.smugmugrc by default, and
44
+ used by smugmug for subsequent login attempts.
45
+
46
+ == Login
47
+
48
+ === Security
49
+
50
+ Logins are carried out over https, but all other operations are carried out
51
+ over http. This is currently not user configurable, but may be in future
52
+ versions.
53
+
54
+ === First Login
55
+
56
+ The first login attempt requires and username, and password.
57
+
58
+ sm = SmugMug::SmugMug.new('user@example.net', 'secret')
59
+
60
+ === Subsequent Login
61
+
62
+ Subsequent logins do not require a username/password if ~/.smugmugrc has good
63
+ data.
64
+
65
+ sm = SmugMug::SmugMug.new
66
+
67
+ === Logging
68
+
69
+ All session data can be logged by for debugging purposes. By default, all
70
+ logging information is sent to /dev/null, or a StringIO if /dev/null does not
71
+ exist. (Note: Need a better solution that StringIO)
72
+
73
+ To create a logging instance do the following before logging into SmugMug.
74
+
75
+ $log = Logger.new('smugmug.log')
76
+ sm = SmugMug::SmugMug.new
77
+
78
+ NOTE: If logging is enabled personal information is captured, most notable the
79
+ username and password, or UserID and password hash depending how login was
80
+ handled.
81
+
82
+ = Albums
83
+
84
+ Use the each method to iterate over your albums. You can also use any?, and
85
+ size to determine if there are albums, and the number of albums. Because the
86
+ each method is provided, you can use any enumerable method.
87
+
88
+ sm = SmugMug::SmugMug.new
89
+
90
+ if sm.any?
91
+ puts "You have albums."
92
+ else
93
+ puts "You have no albums."
94
+ end
95
+
96
+ puts "There are #{sm.size} albums"
97
+
98
+ sm.each do |album|
99
+ puts albums.title
100
+ end
101
+
102
+ == Methods
103
+
104
+ Each albums has the following methods. The alias for each method is also shown
105
+ for completeness.
106
+
107
+ (Some of these functions are self explanatory, but others are not. I need to
108
+ clarify these.)
109
+
110
+ === Standard
111
+
112
+ The following methods are available for Standard, Power, and Pro, and act on
113
+ the current album.
114
+
115
+ * AlbumID (album_id): returns the Album ID.
116
+ * HighlightID (hightlight_id): returns the hightlight ID.
117
+ * CategoryID (category_id): returns the category ID.
118
+ * SubCategoryID (sub_category_id): returns the sub category ID.
119
+ * Position (position): returns the position.
120
+ * SortMethod (sort_method): return sort method: Position, Caption, Filenames, Date Uploaded, Date Modified, Date Taken.
121
+ * SortDirection (sort_direction?): sort ascending (true), or descending (false).
122
+ * ImageCount (image_count): returns the number of images.
123
+ * Keywords (keywords): returns the keywords.
124
+ * Description (description): returns the description.
125
+ * LastUpdate (last_updated): returns the date and time when the album was last updated.
126
+ * CommunityID (community_id): returns the community id.
127
+ * Public (public?): show this gallery on your homepage?
128
+ * Password (password): returns the password.
129
+ * PasswordHint (password_hint): return password hint.
130
+ * CanRank (can_rank?): allow visitors to vote your photos most popular?
131
+ * Printable (printable?): can people buy prints of these photos?
132
+ * Filenames (filenames?): for photos without captions, show the filename instead of nothing?
133
+ * Comments (comments?): can visitors read and post comments?
134
+ * External (external?): allow external links?
135
+ * Originals (originals?): can people view your original full-size photos?
136
+ * FamilyEdit (family_edit?): can your family edit captions and keywords?
137
+ * FriendEdit (friend_edit?): can your friends edit captions and keywords?
138
+ * HideOwnder (hide_owner?): hide your name, navigation, and look and feel?
139
+ * EXIF (exif?): show extra camera info?
140
+ * Share (share?): do you want the 'share photo: links formus, blogs' link?
141
+ * Header (header?): (???)
142
+ * SmugSearchable (smug_searchable?): do you want your photos in SmugMug's search?
143
+ * WorldSearchable (world_searchable?): can Google find your smug mug?
144
+
145
+ puts album.image_count
146
+ puts album.SortMethod
147
+
148
+ === Power
149
+
150
+ The following methods are available for Power, and Pro. (Need help with these.)
151
+
152
+ * Header (header?): (???)
153
+ * TemplateID (template_id): (???)
154
+
155
+ puts album.header
156
+
157
+ === Pro
158
+
159
+ The following methods are available for Pro. (Need help with these.)
160
+
161
+ * Larges (larges?): (???)
162
+ * DefaultColor (default_color?): (???)
163
+ * Clean (clean?): (???)
164
+ * Protected (protected?): (???)
165
+ * Watermark (watermark?): (???)
166
+
167
+ puts album.Watermark
168
+ puts album.watermark?
169
+
170
+ == Stats
171
+
172
+ Every album has statistics associted with it for the current month. SmugMug
173
+ supports the retreival of statistics from up to two months previous, but
174
+ smugmug only supports the current month.
175
+
176
+ stats = album.stats
177
+ puts stats.bytes
178
+ puts album.stats.small
179
+
180
+ === Methods
181
+
182
+ The following methods are supported for the an Album's statistics. (I need to
183
+ verify if these definitions are correct.)
184
+
185
+ * Bytes (bytes): return bytes downloaded for this album.
186
+ * Tiny (tiny): retuns the number of tiny images fetched.
187
+ * Thumb (thumb): retuns the number of thumb images fetched.
188
+ * Small (small): retuns the number of small images fetched.
189
+ * Medium (medium): retuns the number of medium images fetched.
190
+ * Large (large): retuns the number of large images fetched.
191
+ * Original (original): retuns the number of original images fetched.
192
+
193
+ Note: album statistics also have a status, but that information is not returned
194
+ by smugmug. (I am not really sure what status means.)
195
+
196
+ = Images
197
+
198
+ Just like the Album class the methods any?, size, and each are available.
199
+ These methods are attached to an album instance. Likewise, album instances
200
+ include Enumerable and can be used with any of the enumerable methods, such as
201
+ inject.
202
+
203
+ if album.any?
204
+ puts "This album contains images."
205
+ else
206
+ puts "This album contains no images."
207
+ end
208
+
209
+ puts "This album contains #{album.size} images."
210
+ puts "This album contains #{album.image_count} images."
211
+
212
+ album.each do |image|
213
+ puts "#{image.file_name} has MD5 Sum #{image.md5_sum}"
214
+ end
215
+
216
+ === Methods
217
+
218
+ Just like with Album there are two functions to access the same values. One
219
+ based on SmugMug's name, and based on ruby's convention.
220
+
221
+ * AlbumID (album_id): returns this image's album ID.
222
+ * ImageID (image_id): returns this image's ID.
223
+ * Position (position): returns this image's position.
224
+ * Serial (serial): returns this image's serial.
225
+ * Size (size): returns this image's size in bytes.
226
+ * Width (width): return this image's width in pixels.
227
+ * Height (height): return this image's height in pixels.
228
+ * LastUpdated (last_updated): returns the date and time the last time this image was updated.
229
+ * Caption (caption): returns the caption of this image.
230
+ * FileName (file_name): returns this image's file name.
231
+ * MD5Sum (md5_sum): returns this image's MD5 sum.
232
+ * Watermark (watermark): returns this image's watermark.
233
+ * Format (format): returns this image's format, such as JPG.
234
+ * Keywords (keywords): returns this image's keywords.
235
+ * Date (date): returns the date and time this image was taken (???)
236
+ * OriginalURL (original_url): return a URL pointing this image's original.
237
+ * LargeURL (Large_url): return a URL pointing a large version of this image's.
238
+ * MediumURL (medium_url): return a URL pointing a medium version of this image's.
239
+ * SmallURL (small_url): return a URL pointing a small version of this image's.
240
+ * ThumbURL (thumb_url): return a URL pointing a thumb version of this image's.
241
+ * TinyURL (tiny_url): return a URL pointing a tiny version of this image's.
242
+ * AlbumURL (album_url): return a URL pointing to this image's album.
243
+
244
+ puts "This image is #{image.size} bytes."
245
+
246
+ == EXIF
247
+
248
+ The method exif is attached to every image instance, and returns the EXIF class
249
+ attached to this image.
250
+
251
+ exif = image.exif
252
+ puts "The camera that took this image was #{exif.model}."
253
+
254
+ === Methods
255
+
256
+ EXIF methods are unique in that SmugMug does not guarantee that all methods are
257
+ available. This make sense because not all camera manfucaturers report the
258
+ same data. If you attempt to access data that does not exist the value nil is
259
+ returned.
260
+
261
+ if image.exif.iso.nil?
262
+ puts "Sorry, no EXIF data is available for 'ISO'"
263
+ end
264
+
265
+ All functions are in the context of this image.
266
+
267
+ * ImageId (image_id): returns the image ID.
268
+ * DateTime (date_time): returns the date and time.
269
+ * DateTimeOriginal (date_time_original): (???)
270
+ * DateTimeDigitized (date_time_digitized): returns the date and time when this image was digitized.
271
+ * Make (make): returns the make of the camera that took.
272
+ * Model (model): returns the model of the camera that took.
273
+ * ExposureTime (exposure_time): returns the exposure time.
274
+ * Aperture (aperture): returns the aperture.
275
+ * ISO (iso): returns the ISO.
276
+ * FocalLength (focal_lenght): returns the focal length.
277
+ * FocalLength35mmFilm (focal_length35mm_film): (???)
278
+ * CCDWidth (ccdwidth): return the CCD width
279
+ * CompressedBitsPerPixel (compressed_bits_per_pixel): returns the compressed bits per pixel.
280
+ * Flash (flash): (???)
281
+ * Metering (metering): (???)
282
+ * ExposureProgram (exposure_program): (???)
283
+ * SubjectDistance (subject_distance): return the distance to the subject.
284
+ * SubjectDistanceRange (subject_distance_range): return an range of the distance to the subject.
285
+ * SensingMethod (sensing_method): returns the sensing method.
286
+ * ColorSpace (color_space): returns the color space.
287
+ * Brightness (brightness): returns the brigtness.
288
+
289
+ exif = image.exif
290
+ puts "This image was taked on #{exif.date_time}."
291
+
292
+ == Stats
293
+
294
+ Image stats are identical to album stats except that they apply to a specific
295
+ image.
296
+
297
+ === Methods
298
+
299
+ The following methods are supported for the an image's statistics. (I need to
300
+ verify if these definitions are correct.)
301
+
302
+ * Bytes (bytes): return bytes downloaded.
303
+ * Tiny (tiny): downloads of the tiny representation.
304
+ * Thumb (thumb): downloads of the thumb representation.
305
+ * Small (small): downloads of the small representation.
306
+ * Medium (medium): downloads of the medium representation.
307
+ * Large (large): downloads of the large representation.
308
+ * Original (original): downloads of the original.
309
+
310
+ puts "The original of this image was downloaded #{image.stats.original} times for #{Time.now.month}/#{Time.now.year}"
data/doc/TODO ADDED
@@ -0,0 +1,32 @@
1
+ # -*- text -*-
2
+ # $Hg$
3
+
4
+ == Documentation
5
+
6
+ * You'll never have enough!
7
+
8
+ == Library
9
+
10
+ * More testing
11
+ a. extend the SmugMug servlet
12
+ a. make login more realistic
13
+ a. automatically generate fake test data
14
+ a. use SQLite has the backend for the servlet
15
+ * Add write support
16
+ a. adding new albums
17
+ a. uploading new images
18
+ a. modifying album, and image attributes
19
+ * Add support for categories, and sub categories
20
+ * Add support for password protected galleries
21
+ * Add support for a date type to MethodTable.
22
+ * Add support for explicitly specifying an alias for a MethodTable.
23
+ * Add support for scrubbing the data generated by canned.
24
+ * Factor StatsBase out into base/stats.rb
25
+
26
+ == Scripts
27
+
28
+ * Add a script to query *all* of the SmugMug data and stick it in a
29
+ SQLite database.
30
+ * Add a script to verify the integrity of one's photos.
31
+ * Add a poor man's rsync script to upload missing photos to SmugMug.
32
+ * Add a script to move photos between SmugMug and Picasa.
data/lib/net/httpz.rb ADDED
@@ -0,0 +1,31 @@
1
+ # -*- ruby -*-
2
+ # $Hg$
3
+
4
+ require 'net/https'
5
+ require 'stringio'
6
+ require 'zlib'
7
+
8
+ module Net
9
+ class HTTP
10
+ alias original_get get
11
+ def get(path, initheader = nil, dest = nil, &block)
12
+ headers = { 'Accept-Encoding' => 'gzip' }
13
+ headers.merge!(initheader) unless initheader.nil?
14
+
15
+ return original_get(path, headers, dest, &block)
16
+ end
17
+ end
18
+
19
+ class HTTPResponse
20
+ def gzip?
21
+ return false unless @header.has_key?('content-encoding')
22
+ return @header['content-encoding'].to_s == 'gzip'
23
+ end
24
+
25
+ alias original_body body
26
+ def body
27
+ data = original_body
28
+ return (gzip?) ? Zlib::GzipReader.new(StringIO.new(data)).read : data
29
+ end
30
+ end
31
+ end
data/lib/smugmug.rb ADDED
@@ -0,0 +1,179 @@
1
+ #!/usr/bin/env ruby
2
+ # $Hg: smugmug.rb,v c21b2f4bec8c 2007/08/11 08:00:21 boumenot $
3
+
4
+ require 'forwardable'
5
+ require 'logger'
6
+ require 'net/httpz'
7
+ require 'rexml/document'
8
+ require 'stringio'
9
+ require 'time'
10
+ require 'uri'
11
+ require 'zlib'
12
+
13
+ require 'smugmug/albums'
14
+ require 'smugmug/cache'
15
+ require 'smugmug/config'
16
+ require 'smugmug/util'
17
+
18
+ module SmugMug
19
+
20
+ API_VERSION='1.1.1'
21
+ API_KEY='dydlCYDzmykqoLkVGgECEIP5WEuwWFzc'
22
+ URL="https://api.smugmug.com/hack/rest/#{API_VERSION}/"
23
+
24
+ VERSION = '0.0.1'
25
+ USERAGENT = "SmugMug Ruby Library - #{VERSION} " + '($HgRev: c21b2f4bec8ce187d5bcb5cd75a6076c40c0cf5f $)'
26
+
27
+ class SmugMugError < RuntimeError
28
+ end
29
+
30
+ class NotModified < SmugMugError
31
+ end
32
+
33
+ class SmugMug
34
+ include Enumerable
35
+
36
+ extend Forwardable
37
+ def_delegator :@config, :AccountType, :account_type
38
+ def_delegator :albums, :each, :each
39
+ def_delegator :albums, :any?, :any?
40
+ def_delegator :albums, :size, :size
41
+
42
+ attr_reader :base, :session_id
43
+
44
+ def initialize(*args)
45
+ if $log.nil?
46
+ $log = if File.exists?('/dev/null')
47
+ Logger.new('/dev/null')
48
+ else
49
+ Logger.new(StringIO.new)
50
+ end
51
+ end
52
+
53
+ @base = ENV.has_key?('SMUGMUG_URL') ? ENV['SMUGMUG_URL'] : URL
54
+ @config = Config.new()
55
+ login(*args)
56
+ end
57
+
58
+ def fetch(params={})
59
+ query = build_url(params)
60
+ data = get(query)
61
+ doc = REXML::Document.new(data)
62
+
63
+ if doc.root.attributes['stat'] == 'fail'
64
+ raise SmugMugError,
65
+ "#{doc.root.elements['err'].attributes['code']}" +
66
+ "- #{doc.root.elements['err'].attributes['msg']}"
67
+ end
68
+
69
+ return doc
70
+ end
71
+
72
+ private
73
+
74
+ def albums
75
+ @albums = Albums.new(self) if @albums.nil?
76
+ return @albums
77
+ end
78
+
79
+ def build_url(params)
80
+ query = base + '?'
81
+
82
+ $log.debug('SmugMug#build_url') { "Params:" }
83
+ params.each do |k,v|
84
+ $log.debug('SmugMug#build_url') { " #{k}=#{v}" }
85
+ query += "#{k}=" + URI.encode(v.to_s) + '&'
86
+ end
87
+
88
+ return query
89
+ end
90
+
91
+ public
92
+
93
+ def get(url, headers={})
94
+ # XXX: this is a hack, think of a better solution
95
+ url.sub!(/^https:/, 'http:') unless url =~ /method=smugmug\.login/
96
+ $log.info('SmugMug#get') { url }
97
+
98
+ headers['User-Agent'] = USERAGENT
99
+ uri = URI.parse(url)
100
+
101
+ http = Net::HTTP.new(uri.host, uri.port)
102
+
103
+ if uri.scheme == 'https'
104
+ http.use_ssl = true
105
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
106
+ end
107
+
108
+ http.start {
109
+ response = http.get(uri.path + '?' + uri.query, headers)
110
+
111
+ $log.info('SmugMug#get') { "HTTP #{response.code} (#{response.message})" }
112
+ $log.debug('SmugMug#get') { "Headers (response):" }
113
+ response.each_header { |k,v| $log.debug('SmugMug#get') { " #{k}: #{v}" } }
114
+
115
+ case response.code.to_i
116
+ when 200
117
+ body = response.body
118
+ $log.debug('SmugMug#get') { body }
119
+ return body
120
+ when 304
121
+ $log.debug('SmugMug#get') { 'If-Modified-Since is false' }
122
+ raise NotModified, "#{url} has not changed"
123
+ else
124
+ raise SmugMugError, "GET of #{url} failed"
125
+ end
126
+ }
127
+ end
128
+
129
+ def post(url, body, headers={})
130
+ end
131
+
132
+ def login(*args)
133
+ params = {}
134
+ if @config.PasswordHash.nil?
135
+ raise SmugMugError, "Login by password requires a email address, and password" if args.size != 2
136
+
137
+ params[:method] = 'smugmug.login.withPassword'
138
+ params[:EmailAddress] = args.shift
139
+ params[:Password] = args.shift
140
+
141
+ @config.EmailAddress = params[:EmailAddress]
142
+ else
143
+ params[:method] = 'smugmug.login.withHash'
144
+ %w{UserID PasswordHash}.each do |var|
145
+ params[var.to_sym] = @config.send(var.to_sym)
146
+ end
147
+ end
148
+
149
+ params[:APIKey] = API_KEY
150
+ @login_doc = fetch(params).root.elements['Login']
151
+
152
+ @session_id = @login_doc.elements['SessionID'].text
153
+
154
+ Config::VARIABLES.each do |var|
155
+ @config.send("#{var}=".to_sym, @login_doc.elements[var].text) unless @login_doc.elements[var].nil?
156
+ end
157
+ @config.save
158
+ end
159
+ end
160
+ end
161
+
162
+
163
+ def main
164
+ unless ARGV.length == 2
165
+ puts "#{$0} Usage: <email> <password>"
166
+ Kernel.exit(1)
167
+ end
168
+
169
+ File.unlink('smugmug.log') if File.exists?('smugmug.log')
170
+ $log = Logger.new('smugmug.log')
171
+ sm = SmugMug::SmugMug.new(ARGV.shift, ARGV.shift)
172
+ sm.each do |album|
173
+ puts "#{album.id} -> #{album.title} (#{album.ImageCount})"
174
+ end
175
+ end
176
+
177
+ if __FILE__ == $0
178
+ main()
179
+ end