smugmug 0.0.1

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 (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