rscribd 0.0.2 → 0.0.3

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/lib/scribd/base.rb DELETED
@@ -1,381 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'md5'
4
- require 'rexml/document'
5
- require 'net/http'
6
- require 'parsedate'
7
-
8
- # Main Scribd API class
9
- class Scribd
10
- # Default API key
11
- API_KEY = ''
12
-
13
- # Default API signature
14
- API_SIG = ''
15
-
16
- # Default API endpoint
17
- ENDPOINT = 'http://tikhon.com/api'
18
-
19
- # Number of tries to call a method
20
- TRIES = 3
21
-
22
- attr_reader :api_key, :host, :port, :path
23
- attr_accessor :async, :debug
24
-
25
- # Outputs whatever is given into the $stderr if debugging is enabled.
26
- #
27
- # Parameters:
28
- # args - content to output
29
- def debug(*args) $stderr.puts(sprintf(*args)) if @debug end
30
-
31
- # Initializes API
32
- #
33
- # Parameters:
34
- # api_key - API key to use
35
- # api_sig - API signature to use
36
- # endpoint - API endpoint,
37
- def initialize(api_key = API_KEY, api_sig = API_SIG, endpoint = ENDPOINT)
38
- @async = false
39
-
40
- @api_key = api_key
41
- @api_sig = api_sig
42
- @endpoint = endpoint
43
-
44
- proto, @host, @port, @path, user, pass = parse_url(@endpoint)
45
- raise ProtoUnknownError.new("Unhandled protocol '#{proto}'") if proto.downcase != 'http'
46
- end
47
-
48
- # Returns Docs API interface.
49
- def docs() @docs ||= Docs.new(self) end
50
-
51
- # Returns Search API interface.
52
- def search() @search || Search.new(self) end
53
-
54
- # Sends the HTTP form to the server API with the given fields.
55
- #
56
- # Parameters:
57
- # method - method name.
58
- # fields - the Hash of field names to field values. If a field requires
59
- # its mime-type set, you specify its value in array where the
60
- # first element is value, the second is mime-type, and the third
61
- # is file name, like this:
62
- # Simple value: :user => 'john', :pass => 'nhoj'
63
- # Typed value : :user => 'john', :file => [data, type, name]
64
- # or : ... :file => { :data => data, :mimetype => type, :filename => 'test.txt' }
65
- #
66
- # Returns:
67
- # REXML::Document with the response of the server.
68
- #
69
- # Raises:
70
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
71
- # Timeout::Error if unable to connect to the server.
72
- #
73
- def send_form(method, fields)
74
- # See if method is given
75
- raise ArgumentError, "Method should be given" if method.nil? || method.empty?
76
-
77
- # Complete fields with the method name
78
- fields = {} if fields.nil?
79
- fields['method'] = method
80
-
81
- # Create a multi-part form from given fields
82
- form = Scribd::MultiPartForm.new
83
- form.parts = prepare_parts(fields)
84
-
85
- debug(form.to_s)
86
-
87
- # Configure proper boundary in the headers
88
- headers = { "Content-Type" => "multipart/form-data; boundary=" + form.boundary }
89
-
90
- # Create the connection
91
- http = Net::HTTP.new(host, port)
92
- # TODO configure timeouts through the properties
93
- # http.read_timeout = 900 # 15 minutes max upload time
94
-
95
- tries = TRIES
96
- begin
97
- tries -= 1
98
- res = http.post(path, form.to_s, headers)
99
- rescue Timeout::Error => err
100
- $stderr.puts "Timed out, will retry #{tries} more."
101
- retry if tries > 0
102
- raise err
103
- end
104
-
105
- debug(res.body)
106
-
107
- # Convert response into XML
108
- xml = REXML::Document.new(res.body)
109
-
110
- # See if there was an error and raise an exception
111
- if xml.elements['/rsp'].attributes['stat'] == 'fail'
112
- # Load default code and error
113
- code, message = -1, "Unidentified error:\n#{res.body}"
114
-
115
- # Get actual error code and message
116
- err = xml.elements['/rsp/error']
117
- code, message = err.attributes['code'], err.attributes['message'] if err
118
-
119
- # Add more data
120
- message = "Method: #{method} Response: code=#{code} message=#{message}"
121
-
122
- raise Scribd::ResponseError.new(code), message
123
- end
124
-
125
- return xml
126
- end
127
-
128
- private
129
-
130
- # Sign the arguments hash with our very own signature.
131
- #
132
- # Parameters:
133
- # args - method arguments to be sent to the server API
134
- #
135
- # Returns:
136
- # signature
137
- #
138
- def sign(args)
139
- return MD5.md5(@api_sig + args.sort.flatten.join).to_s
140
- end
141
-
142
- # Parses given URL into pieces
143
- #
144
- # Parameters:
145
- # url - URL to parse
146
- #
147
- # Returns:
148
- # array of protocol, host, port, path, user, and password
149
- #
150
- def parse_url(url)
151
- url =~ /([^:]+):\/\/([^\/]*)(.*)/
152
- proto = $1.to_s
153
- hostplus = $2.to_s
154
- path = $3.to_s
155
-
156
- hostplus =~ /(?:(.*)@)?(.*)/
157
- userpass = $1
158
- hostport = $2
159
- user, pass = userpass.to_s.split(':', 2)
160
- host, port = hostport.to_s.split(':', 2)
161
- port = port ? port.to_i : 80
162
-
163
- return proto, host, port, path, user, pass
164
- end
165
-
166
- # Prepares a list of parts for a multi-part form.
167
- # The signature is added to the end of the list as a part.
168
- # Fields with mime-type set are ignored during the signature calculation.
169
- #
170
- # Parameters:
171
- # fields - the Hash of field names to field values. If a field requires
172
- # its mime-type set, you specify its value in array where the
173
- # first element is value, the second is mime-type, and the third
174
- # is file name, like this:
175
- # Simple value: :user => 'john', :pass => 'nhoj'
176
- # Typed value : :user => 'john', :file => [data, type, name]
177
- # or : ... :file => { :data => data, :mimetype => type, :filename => 'test.txt' }
178
- #
179
- # Returns:
180
- # array of Scribd::FormPart objects
181
- #
182
- def prepare_parts(fields)
183
- # Parameter check
184
- raise "Fields must be a Hash" unless fields.nil? || fields.class == Hash
185
-
186
- # Build the list of form parts and the list of non-typed
187
- # arguments in parallel. The list of arguments will be used
188
- # for the signature calculation.
189
- parts = []
190
- args = {}
191
- fields.each do |k, v|
192
- if v.class == Array
193
- data, mimetype, filename = v
194
- elsif v.class == Hash
195
- data, mimetype, filename = v[:data], v[:mimetype], v[:filename]
196
- else
197
- data = v.to_s
198
- end
199
-
200
- parts << Scribd::FormPart.new(k.to_s, data, mimetype, filename)
201
-
202
- # We don't place fields with mime-type set into the array
203
- # as they usually are files and other binary data we don't
204
- # want to hash.
205
- args[k.to_s] = v.to_s if mimetype.nil?
206
- end
207
- parts << Scribd::FormPart.new('api_key', api_key)
208
-
209
- # Add the key and calculate the signature
210
- args['api_key'] = api_key
211
- parts << Scribd::FormPart.new('api_sig', sign(args))
212
-
213
- return parts
214
- end
215
- end
216
-
217
- # Base class for API groups
218
- class Scribd::APIBase
219
- attr_reader :scribd
220
-
221
- # Initializes API group
222
- def initialize(scribd) @scribd = scribd end
223
-
224
- protected
225
-
226
- # FIXME: Since we don't need XMLRPC, the exception could be different
227
- # TODO: It would probably be better if we wrapped the fault
228
- # in something more meaningful. At the very least, a broad
229
- # division of errors, such as retryable and fatal.
230
- def error(el)
231
- att = el.attributes
232
- fe = XMLRPC::FaultException.new(att['code'].to_i, att['msg'])
233
- $stderr.puts "ERR: #{fe.faultString} (#{fe.faultCode})"
234
- raise fe
235
- end
236
-
237
- # Checks if a string parameter is given and not empty.
238
- #
239
- # Parameters:
240
- # name - parameter name for an error message.
241
- # value - value.
242
- #
243
- # Raises:
244
- # ArgumentError if the value is nil, or empty.
245
- #
246
- def check_not_empty(name, value)
247
- check_given(name, value)
248
- raise ArgumentError, "#{name} must not be empty" if value.to_s.empty?
249
- end
250
-
251
- # Checks an integer parameter for being given, being a number,
252
- # and being within the range.
253
- #
254
- # Parameters:
255
- # name - parameter name for an error message.
256
- # value - value.
257
- # min - range minimum.
258
- # max - range maximum.
259
- #
260
- # Raises:
261
- # ArgumentError if the value is nil, not Fixnum, or outside the [min, max] range.
262
- #
263
- def check_in_range(name, value, min, max)
264
- check_given(name, value)
265
- raise ArgumentError, "#{name} is not a number" if value.class != Fixnum
266
- raise ArgumentError, "#{name} is out of range [#{min}, #{max}]" if value < min || value > max
267
- end
268
-
269
- # Checks the value for being given, and being mentioned in the list.
270
- #
271
- # Parameters:
272
- # name - parameter name for an error message.
273
- # value - value.
274
- # allowed - list of allowed values.
275
- #
276
- # Raises:
277
- # ArgumentError if the value is nil, or not among the allowed values.
278
- #
279
- def check_allowed(name, value, allowed)
280
- check_given(name, value)
281
- raise ArgumentError, "#{name} is out of range #{allowed.inspect}" unless allowed.include?(value.to_sym)
282
- end
283
-
284
- # Checks if the value is given.
285
- #
286
- # Parameters:
287
- # name - parameter name for an error message.
288
- # value - value.
289
- #
290
- # Raises:
291
- # ArgumentError if the value is nil.
292
- #
293
- def check_given(name, value)
294
- raise ArgumentError, "#{name} must be given" if value.nil?
295
- end
296
- end
297
-
298
- # Form part class represents a single block of attributes, or
299
- # file or other part of the form.
300
- class Scribd::FormPart
301
- attr_reader :data, :mime_type, :attributes
302
-
303
- # Initializes the part.
304
- #
305
- # Parameters:
306
- # name - name
307
- # data - data
308
- # mime_type - optional type of the form part
309
- # filename - optional name of the file (use with mime-type)
310
- #
311
- def initialize(name, data, mime_type = nil, filename = nil)
312
- @attributes = {}
313
- @attributes['name'] = name
314
- @attributes['filename'] = filename unless filename.nil?
315
- @data = data
316
- @mime_type = mime_type
317
- end
318
-
319
- # Returns string representation.
320
- def to_s
321
- mime_present = @mime_type.nil? || @mime_type.empty?
322
-
323
- # Disposition
324
- fld_disposition = "Content-Disposition: form-data"
325
- attributes.each { |k, v| fld_disposition += "; #{k}=\"#{v}\"" }
326
-
327
- # Fields
328
- fields = [ fld_disposition ]
329
- unless mime_present
330
- fields << "Content-Type: #{@mime_type}"
331
- fields << "Content-Transfer-Encoding: binary"
332
- end
333
-
334
- # Add empty line and data block
335
- fields << ''
336
- fields << data
337
-
338
- return fields.join("\r\n")
339
- end
340
- end
341
-
342
- # Multi-part form object.
343
- class Scribd::MultiPartForm
344
- attr_accessor :boundary, :parts
345
-
346
- # Creates the form object.
347
- #
348
- # Parameters:
349
- # boundary - optional boundary to use for parts separation
350
- #
351
- def initialize(boundary = nil)
352
- @boundary = boundary || "----------------------------Ruby#{rand(1000000000000)}"
353
- @parts = []
354
- end
355
-
356
- # Returns string representation.
357
- def to_s
358
- "--#@boundary\r\n" +
359
- parts.map { |p| p.to_s }.join("\r\n--#@boundary\r\n") +
360
- "\r\n--#@boundary--\r\n"
361
- end
362
- end
363
-
364
- # Response error that indicates there was a problem during
365
- # processing your request to the server, which resulted in
366
- # the response with the 'fail' code.
367
- #
368
- # You can find the code and the message inside.
369
- #
370
- class Scribd::ResponseError < RuntimeError
371
- attr_reader :code
372
-
373
- # Initializes the error.
374
- #
375
- # Parameters:
376
- # code - error code.
377
- #
378
- def initialize(code)
379
- @code = code
380
- end
381
- end
data/lib/scribd/docs.rb DELETED
@@ -1,246 +0,0 @@
1
- require 'rubygems'
2
- require 'scribd/base'
3
- require 'mime/types'
4
-
5
- # Interface to the Docs server API
6
- class Scribd::Docs < Scribd::APIBase
7
-
8
- DISPLAY_FORMAT = [:html, :flash]
9
- PARENTAL_ADVISORY = [:safe, :adult]
10
- BOOLEAN = [:true, :false]
11
- ACCESS = [:public, :private]
12
- COMMENTING = [:none, :allow]
13
-
14
- # Settings with their allowed values
15
- SETTINGS = {
16
- :display_format => DISPLAY_FORMAT,
17
- :parental_advisory => PARENTAL_ADVISORY,
18
- :access => ACCESS,
19
- :commenting => COMMENTING,
20
- :allow_word_download => BOOLEAN,
21
- :allow_text_download => BOOLEAN,
22
- :allow_pdf_download => BOOLEAN
23
- }
24
-
25
- # Uploads a file specified by its name (path) and returns the
26
- # pair of document ID and GUID as an array. In addition to the
27
- # file name, you need to specify document and access types.
28
- #
29
- # Parameters:
30
- # filename - name of the file to upload.
31
- # doc_type - optional type of the document:
32
- # "pdf", "doc", "txt", "ppt", "pps", "xls", "ps", "htm",
33
- # "html", "rtf", "odt", "odp", "ods", "odg", "odf", "sxw",
34
- # "sxc", "sxi", "sxd", "jpg", "jpeg", "gif", "png"
35
- # public - optional flag showing if the document is public (default)
36
- # or private.
37
- #
38
- # Returns:
39
- # array with two elements: doc_id and doc_guid
40
- #
41
- # Raises:
42
- # ArgumentError if file name isn't given.
43
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
44
- # Timeout::Error if unable to connect to the server.
45
- #
46
- def upload(filename, doc_type = nil, public = nil)
47
- # See if URL is given
48
- check_not_empty('filename', filename)
49
-
50
- # Get file type and load its data
51
- mimetype = MIME::Types.of(filename)
52
- mimetype = 'application/octet-stream' if mimetype.nil? || mimetype.empty?
53
- data = File.open(filename, 'rb').read
54
-
55
- # Make a request form
56
- fields = {}
57
- fields[:file] = { :data => data, :mimetype => mimetype,
58
- :filename => File.basename(filename) }
59
-
60
- return upload_impl('docs.upload', fields, doc_type, public)
61
- end
62
-
63
- # Uploads a file specified by an URL and returns the
64
- # pair of document ID and GUID as an array. In addition to the
65
- # file name, you need to specify document and access types.
66
- #
67
- # Parameters:
68
- # url - URL of the file to upload.
69
- # doc_type - optional type of the document:
70
- # "pdf", "doc", "txt", "ppt", "pps", "xls", "ps", "htm",
71
- # "html", "rtf", "odt", "odp", "ods", "odg", "odf", "sxw",
72
- # "sxc", "sxi", "sxd", "jpg", "jpeg", "gif", "png"
73
- # public - optional flag showing if the document is public (default)
74
- # or private.
75
- #
76
- # Returns:
77
- # array with two elements: doc_id and doc_guid
78
- #
79
- # Raises:
80
- # ArgumentError if URL isn't given.
81
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
82
- # Timeout::Error if unable to connect to the server.
83
- #
84
- def upload_from_url(url, doc_type = nil, public = nil)
85
- # See if URL is given
86
- check_not_empty('url', url)
87
-
88
- # Make a request form
89
- fields = {}
90
- fields[:url] = url
91
-
92
- return upload_impl('docs.uploadFromUrl', fields, doc_type, public)
93
- end
94
-
95
- # Deletes a document from the repository by its ID.
96
- #
97
- # Parameters:
98
- # doc_id - document ID.
99
- #
100
- # Returns:
101
- # id of the deleted document.
102
- #
103
- # Raises:
104
- # ArgumentError if document ID isn't given.
105
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
106
- # Timeout::Error if unable to connect to the server.
107
- #
108
- def delete(doc_id)
109
- # See if document ID is given
110
- raise ArgumentError, "Document ID must be given" if doc_id.nil?
111
-
112
- # Send form and return the result
113
- res = @scribd.send_form('docs.delete', :doc_id => doc_id)
114
- return res.elements['/rsp/doc_id'].text
115
- end
116
-
117
- # Returns the status of the document conversion.
118
- #
119
- # Parameters:
120
- # doc_id - document ID.
121
- #
122
- # Returns:
123
- # status string: "CONVERSION_ERROR", "CONVERTING", "ENCRYPTED_ERROR",
124
- # "FAILED_TO_CONVERT", "IN_QUEUE", "PUBLISHED", "UPLOADED".
125
- #
126
- # Raises:
127
- # ArgumentError if document ID isn't given.
128
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
129
- # Timeout::Error if unable to connect to the server.
130
- #
131
- def get_conversion_status(doc_id)
132
- # See if document ID is given
133
- check_given('doc_id', doc_id)
134
-
135
- # Send form and return the result
136
- res = @scribd.send_form('docs.getConversionStatus', :doc_id => doc_id)
137
- return res.elements['/rsp/conversion_status'].text
138
- end
139
-
140
- # Changes the settings of the document specified by its ID. The settings
141
- # is a Hash with any number of settings key-pairs from the list below. If
142
- # no settings are given, the method call isn't relayed to the server for
143
- # the performance reasons.
144
- #
145
- # Settings keys and values you can update with this method:
146
- # title - document title.
147
- # description - document description.
148
- # display_format - "html" (for plain text, HTML and images),
149
- # "flash" (for everything else).
150
- # access - "public" or "private"
151
- # language - "ar", "bg", "da", "de", "en", "es", "fr", "he",
152
- # "hu", "it", "ja", "ko", "nl", "no", "pt", "ro",
153
- # "ru", "unknown" (default), "zh"
154
- # license - "by", "by-nc" (Creative Commons), "by-nc-nd",
155
- # "by-nc-sa", "by-nd", "by-sa",
156
- # "c" (traditional copyright),
157
- # "pd" (public domain)
158
- # allow_word_download - "true" or "false". Allow others to download a
159
- # MS Word version of this document. Default: "true".
160
- # allow_text_download - "true" or "false". Allow others to download a
161
- # plain text version of this document. Default: "true".
162
- # allow_pdf_download - "true" or "false". Allow others to download a
163
- # PDF version of this document. Default: "true".
164
- # commenting - "none" or "allow". Allow others to comment. Default: "allow".
165
- # paretal_advisory - "safe" or "adult". Default: "safe".
166
- # tags - comma-separated list of tags. Tags cannot contain whitespace.
167
- #
168
- # Parameters:
169
- # doc_id - document ID.
170
- # settings - hash with any combination of above settings.
171
- #
172
- # Returns:
173
- # id of the updated document.
174
- #
175
- # Raises:
176
- # ArgumentError if document ID isn't given.
177
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
178
- # Timeout::Error if unable to connect to the server.
179
- #
180
- def change_settings(doc_id, settings = nil)
181
- # See if document ID is given
182
- check_given('doc_id', doc_id)
183
- check_settings(settings)
184
-
185
- # If settings aren't given, return. Otherwise see if they are given in a Hash
186
- return doc_id if settings.nil? || (settings.class == Hash && settings.empty?)
187
- raise ArgumentError, "Settings must be given in a Hash" if settings.class != Hash
188
-
189
- # Copy the settings as we'll be adding more keys to it
190
- fields = settings.dup
191
- fields[:doc_id] = doc_id
192
-
193
- # Send the form and return the result
194
- res = @scribd.send_form('docs.changeSettings', fields)
195
- return res.elements['/rsp/doc_id'].text
196
- end
197
-
198
- private
199
-
200
- # Checks if known settings are inside their allowed lists.
201
- #
202
- # Parameters:
203
- # settings - Hash of settings to check, as given to #change_settings.
204
- #
205
- # Raises:
206
- # ArgumentError if some setting is not given a value, or
207
- # falls out of allowed set.
208
- #
209
- def check_settings(settings)
210
- settings.each do |name, value|
211
- allowed = SETTINGS[name.to_sym]
212
-
213
- # If it's the setting we check, do it
214
- unless allowed.nil?
215
- check_given(name, value)
216
- check_allowed(name, value, allowed)
217
- end
218
- end
219
- end
220
-
221
- # Uploads the file.
222
- #
223
- # Parameters:
224
- # method - method name.
225
- # fields - method arguments.
226
- # doc_type - document type (see concrete methods for description).
227
- # public - public / private boolean flag.
228
- #
229
- # Returns:
230
- # array with two elements: doc_id and doc_guid
231
- #
232
- def upload_impl(method, fields, doc_type, public)
233
- fields = {} if fields.nil?
234
- fields[:doc_type] = doc_type unless doc_type.nil?
235
- fields[:access] = (public ? 'public' : 'private') unless public.nil?
236
-
237
- # Send it and wait for the result
238
- res = @scribd.send_form(method, fields)
239
-
240
- # Extract our response
241
- doc_id = res.elements['/rsp/doc_id'].text
242
- doc_guid = res.elements['/rsp/doc_guid'].text
243
-
244
- return doc_id, doc_guid
245
- end
246
- end
data/lib/scribd/search.rb DELETED
@@ -1,85 +0,0 @@
1
- require 'scribd/base'
2
-
3
- # Interface to the Search server API
4
- class Scribd::Search < Scribd::APIBase
5
-
6
- SORT_BY = [:popularity, :relevance, :scribds, :time, :views]
7
- SCOPE = [:all, :api]
8
- PARENTAL_ADVISORY = [:safe, :adult]
9
-
10
- # Looks for documents matching a given query. Additional optional
11
- # parameters can be used to control the output.
12
- #
13
- # Parameters:
14
- # query - query to pass to the server.
15
- # num_results - desired maximum number of records to return [1-1000].
16
- # num_start - first record in the result set to start from [1-1000].
17
- # sort_by - order of sorting, default :relevance. See SORT_BY
18
- # constant for the list of allowed values.
19
- # scope - scope of search:
20
- # :all - all of Scribd
21
- # :api - only user documents
22
- # parental_advisory - filter for adult content:
23
- # :safe - no adult content
24
- # :adult - everything
25
- #
26
- # Returns:
27
- # list of Scribd::Document objects.
28
- #
29
- # Raises:
30
- # ArgumentError if query is not given, or
31
- # num_results is out of range, or
32
- # num_start is out of range, or
33
- # sort_by is not one of allowed, or
34
- # scope is not one of allowed, or
35
- # parental_advisory is not one of allowed.
36
- # Scribd::ResponseError upon getting the 'fail' code from the server API.
37
- # Timeout::Error if unable to connect to the server.
38
- #
39
- def docs(query, num_results = 10, num_start = 1, sort_by = :relevance,
40
- scope = :all, parental_advisory = :safe)
41
-
42
- # Verify the parameters
43
- check_not_empty('query', query)
44
- check_in_range('num_results', num_results, 1, 1000)
45
- check_in_range('num_start', num_start, 1, 1000)
46
- check_allowed('sort_by', sort_by, SORT_BY)
47
- check_allowed('scope', scope, SCOPE)
48
- check_allowed('parental_advisory', parental_advisory, PARENTAL_ADVISORY)
49
-
50
- # Send the form
51
- res = @scribd.send_form('search.docs', fields)
52
-
53
- # Get documents
54
- documents = []
55
- res.elements['/response/result_set/result'].each do |doc|
56
- documents << Scribd::Document.new(
57
- doc.elements['title'],
58
- doc.elements['description'],
59
- doc.elements['scribd_url'],
60
- doc.elements['doc_id'])
61
- end
62
-
63
- return documents
64
- end
65
- end
66
-
67
- # Document returned from the search
68
- class Scribd::Document
69
- attr_reader :title, :description, :scribd_url, :doc_id
70
-
71
- # Initializes a document.
72
- #
73
- # Parameters:
74
- # title - document title.
75
- # description - description.
76
- # scribd_url - Scribd URL.
77
- # doc_id - document ID.
78
- #
79
- def initialize(title, description, scribd_url, doc_id)
80
- @title = title
81
- @description = description
82
- @scribd_url = scribd_url
83
- @doc_id = doc_id
84
- end
85
- end