fandango 0.2.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.irbrc +5 -0
  2. data/README.md +1 -3
  3. data/fandango.gemspec +4 -20
  4. data/lib/fandango.rb +27 -16
  5. data/lib/fandango/movie.rb +30 -0
  6. data/lib/fandango/parser.rb +34 -19
  7. data/lib/fandango/theater.rb +50 -0
  8. data/lib/fandango/version.rb +1 -1
  9. data/spec/fandango.spec.rb +23 -25
  10. data/spec/movie.spec.rb +24 -0
  11. data/spec/parser.spec.rb +12 -0
  12. data/spec/spec_helper.rb +5 -3
  13. data/spec/support/fixtures/item.html +1 -0
  14. data/spec/support/fixtures/movies_near_me_73142.rss +1 -1
  15. data/spec/support/fixtures/movies_near_me_73142.yaml +349 -0
  16. data/spec/support/helpers.rb +25 -0
  17. data/spec/support/vcr_cassettes/movies_near_me_123BADZIP.yml +48 -0
  18. data/spec/support/vcr_cassettes/movies_near_me_73142.yml +247 -0
  19. data/spec/theater.spec.rb +16 -0
  20. metadata +41 -130
  21. data/lib/fandango/parsers/movie.rb +0 -30
  22. data/lib/fandango/parsers/theater.rb +0 -37
  23. data/lib/feedzirra.rb +0 -4
  24. data/lib/vendor/feedzirra/.gitignore +0 -6
  25. data/lib/vendor/feedzirra/.rspec +0 -1
  26. data/lib/vendor/feedzirra/lib/feedzirra.rb +0 -19
  27. data/lib/vendor/feedzirra/lib/feedzirra/core_ext.rb +0 -3
  28. data/lib/vendor/feedzirra/lib/feedzirra/core_ext/date.rb +0 -19
  29. data/lib/vendor/feedzirra/lib/feedzirra/core_ext/string.rb +0 -9
  30. data/lib/vendor/feedzirra/lib/feedzirra/feed.rb +0 -383
  31. data/lib/vendor/feedzirra/lib/feedzirra/feed_entry_utilities.rb +0 -65
  32. data/lib/vendor/feedzirra/lib/feedzirra/feed_utilities.rb +0 -72
  33. data/lib/vendor/feedzirra/lib/feedzirra/parser.rb +0 -17
  34. data/lib/vendor/feedzirra/lib/feedzirra/parser/atom.rb +0 -29
  35. data/lib/vendor/feedzirra/lib/feedzirra/parser/atom_entry.rb +0 -30
  36. data/lib/vendor/feedzirra/lib/feedzirra/parser/atom_feed_burner.rb +0 -21
  37. data/lib/vendor/feedzirra/lib/feedzirra/parser/atom_feed_burner_entry.rb +0 -31
  38. data/lib/vendor/feedzirra/lib/feedzirra/parser/itunes_rss.rb +0 -50
  39. data/lib/vendor/feedzirra/lib/feedzirra/parser/itunes_rss_item.rb +0 -32
  40. data/lib/vendor/feedzirra/lib/feedzirra/parser/itunes_rss_owner.rb +0 -12
  41. data/lib/vendor/feedzirra/lib/feedzirra/parser/rss.rb +0 -22
  42. data/lib/vendor/feedzirra/lib/feedzirra/parser/rss_entry.rb +0 -34
  43. data/lib/vendor/feedzirra/lib/feedzirra/parser/rss_feed_burner.rb +0 -22
  44. data/lib/vendor/feedzirra/lib/feedzirra/parser/rss_feed_burner_entry.rb +0 -40
  45. data/lib/vendor/feedzirra/lib/feedzirra/version.rb +0 -3
  46. data/spec/support/macros.rb +0 -18
@@ -1,30 +0,0 @@
1
- module Fandango
2
- class Parser::Movie
3
-
4
- def initialize(entry)
5
- @entry = entry
6
- end
7
-
8
- # Return array of movie attributes.
9
- def parse
10
- @entry.summary_doc.css('li').map do |li|
11
- {
12
- title: parse_title(li),
13
- id: parse_id(li),
14
- }
15
- end
16
- end
17
-
18
- private
19
-
20
- def parse_title(li)
21
- li.at_css('a').content
22
- end
23
-
24
- # E.g. '141081' in fandango.com/the+adventures+of+tintin+3d_141081/movietimes
25
- def parse_id(li)
26
- li.at_css('a')['href'].match(%r{fandango\.com/.*_(?<id>\d+)/movietimes})[:id]
27
- end
28
-
29
- end
30
- end
@@ -1,37 +0,0 @@
1
- module Fandango
2
- class Parser::Theater
3
-
4
- def initialize(entry)
5
- @entry = entry
6
- end
7
-
8
- # Compose hash with each attribute as key and result of #parse_<attribute> as value.
9
- def parse
10
- atts = [:name, :id, :address, :postal_code]
11
- atts.each_with_object({}) do |attr, hash|
12
- hash[attr] = send :"parse_#{attr}"
13
- end
14
- end
15
-
16
- private
17
-
18
- def parse_name
19
- @entry[:title]
20
- end
21
-
22
- # E.g. 'aaicu' in http://www.fandango.com/northpark7_aaicu/theaterpage
23
- def parse_id
24
- @entry[:url].match(%r{fandango\.com/.*_(?<id>.*)/theaterpage})[:id]
25
- end
26
-
27
- # Cache address in variable for postal_code.
28
- def parse_address
29
- @address = @entry.summary_doc.at_css('p').content
30
- end
31
-
32
- def parse_postal_code
33
- @address.match(/(?<postal_code>\d+)$/)[:postal_code]
34
- end
35
-
36
- end
37
- end
@@ -1,4 +0,0 @@
1
- # See fandango.gemspec.
2
-
3
- $LOAD_PATH << File.dirname(__FILE__) +'/vendor/feedzirra/lib'
4
- require 'vendor/feedzirra/lib/feedzirra'
@@ -1,6 +0,0 @@
1
- .DS_Store
2
- .rvm
3
- TODO
4
- Gemfile.lock
5
- rdoc/
6
- doc/
@@ -1 +0,0 @@
1
- --color
@@ -1,19 +0,0 @@
1
- require 'zlib'
2
- require 'curb'
3
- require 'sax-machine'
4
- require 'loofah'
5
- require 'uri'
6
-
7
- require 'active_support/basic_object'
8
- require 'active_support/core_ext/module'
9
- require 'active_support/core_ext/object'
10
- require 'active_support/time'
11
-
12
- require 'feedzirra/core_ext'
13
-
14
- module Feedzirra
15
- autoload :FeedEntryUtilities, 'feedzirra/feed_entry_utilities'
16
- autoload :FeedUtilities, 'feedzirra/feed_utilities'
17
- autoload :Feed, 'feedzirra/feed'
18
- autoload :Parser, 'feedzirra/parser'
19
- end
@@ -1,3 +0,0 @@
1
- Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path|
2
- require "feedzirra/core_ext/#{File.basename(path, '.rb')}"
3
- end
@@ -1,19 +0,0 @@
1
- # Date code pulled and adapted from:
2
- # Ruby Cookbook by Lucas Carlson and Leonard Richardson
3
- # Published by O'Reilly
4
- # ISBN: 0-596-52369-6
5
- class Date
6
- def feed_utils_to_gm_time
7
- feed_utils_to_time(new_offset, :gm)
8
- end
9
-
10
- def feed_utils_to_local_time
11
- feed_utils_to_time(new_offset(DateTime.now.offset-offset), :local)
12
- end
13
-
14
- private
15
- def feed_utils_to_time(dest, method)
16
- Time.send(method, dest.year, dest.month, dest.day, dest.hour, dest.min,
17
- dest.sec, dest.zone)
18
- end
19
- end
@@ -1,9 +0,0 @@
1
- class String
2
- def sanitize!
3
- self.replace(sanitize)
4
- end
5
-
6
- def sanitize
7
- Loofah.scrub_fragment(self, :prune).to_s
8
- end
9
- end
@@ -1,383 +0,0 @@
1
- module Feedzirra
2
- class NoParserAvailable < StandardError; end
3
-
4
- class Feed
5
- USER_AGENT = "feedzirra http://github.com/pauldix/feedzirra/tree/master"
6
-
7
- # Takes a raw XML feed and attempts to parse it. If no parser is available a Feedzirra::NoParserAvailable exception is raised.
8
- # You can pass a block to be called when there's an error during the parsing.
9
- # === Parameters
10
- # [xml<String>] The XML that you would like parsed.
11
- # === Returns
12
- # An instance of the determined feed type. By default a Feedzirra::Atom, Feedzirra::AtomFeedBurner, Feedzirra::RDF, or Feedzirra::RSS object.
13
- # === Raises
14
- # Feedzirra::NoParserAvailable : If no valid parser classes could be found for the feed.
15
- def self.parse(xml, &block)
16
- if parser = determine_feed_parser_for_xml(xml)
17
- parser.parse(xml, block)
18
- else
19
- raise NoParserAvailable.new("No valid parser for XML.")
20
- end
21
- end
22
-
23
- # Determines the correct parser class to use for parsing the feed.
24
- #
25
- # === Parameters
26
- # [xml<String>] The XML that you would like determine the parser for.
27
- # === Returns
28
- # The class name of the parser that can handle the XML.
29
- def self.determine_feed_parser_for_xml(xml)
30
- start_of_doc = xml.slice(0, 2000)
31
- feed_classes.detect {|klass| klass.able_to_parse?(start_of_doc)}
32
- end
33
-
34
- # Adds a new feed parsing class that will be used for parsing.
35
- #
36
- # === Parameters
37
- # [klass<Constant>] The class/constant that you want to register.
38
- # === Returns
39
- # A updated array of feed parser class names.
40
- def self.add_feed_class(klass)
41
- feed_classes.unshift klass
42
- end
43
-
44
- # Provides a list of registered feed parsing classes.
45
- #
46
- # === Returns
47
- # A array of class names.
48
- def self.feed_classes
49
- @feed_classes ||= [Feedzirra::Parser::RSSFeedBurner, Feedzirra::Parser::RSS, Feedzirra::Parser::AtomFeedBurner, Feedzirra::Parser::Atom]
50
- end
51
-
52
- # Makes all registered feeds types look for the passed in element to parse.
53
- # This is actually just a call to element (a SAXMachine call) in the class.
54
- #
55
- # === Parameters
56
- # [element_tag<String>] The element tag
57
- # [options<Hash>] Valid keys are same as with SAXMachine
58
- def self.add_common_feed_element(element_tag, options = {})
59
- feed_classes.each do |k|
60
- k.element element_tag, options
61
- end
62
- end
63
-
64
- # Makes all registered feeds types look for the passed in elements to parse.
65
- # This is actually just a call to elements (a SAXMachine call) in the class.
66
- #
67
- # === Parameters
68
- # [element_tag<String>] The element tag
69
- # [options<Hash>] Valid keys are same as with SAXMachine
70
- def self.add_common_feed_elements(element_tag, options = {})
71
- feed_classes.each do |k|
72
- k.elements element_tag, options
73
- end
74
- end
75
-
76
- # Makes all registered entry types look for the passed in element to parse.
77
- # This is actually just a call to element (a SAXMachine call) in the class.
78
- #
79
- # === Parameters
80
- # [element_tag<String>]
81
- # [options<Hash>] Valid keys are same as with SAXMachine
82
- def self.add_common_feed_entry_element(element_tag, options = {})
83
- call_on_each_feed_entry :element, element_tag, options
84
- end
85
-
86
- # Makes all registered entry types look for the passed in elements to parse.
87
- # This is actually just a call to element (a SAXMachine call) in the class.
88
- #
89
- # === Parameters
90
- # [element_tag<String>]
91
- # [options<Hash>] Valid keys are same as with SAXMachine
92
- def self.add_common_feed_entry_elements(element_tag, options = {})
93
- call_on_each_feed_entry :elements, element_tag, options
94
- end
95
-
96
- # Call a method on all feed entries classes.
97
- #
98
- # === Parameters
99
- # [method<Symbol>] The method name
100
- # [parameters<Array>] The method parameters
101
- def self.call_on_each_feed_entry(method, *parameters)
102
- feed_classes.each do |k|
103
- # iterate on the collections defined in the sax collection
104
- k.sax_config.collection_elements.each_value do |vl|
105
- # vl is a list of CollectionConfig mapped to an attribute name
106
- # we'll look for the one set as 'entries' and add the new element
107
- vl.find_all{|v| (v.accessor == 'entries') && (v.data_class.class == Class)}.each do |v|
108
- v.data_class.send(method, *parameters)
109
- end
110
- end
111
- end
112
- end
113
-
114
- # Setup curl from options.
115
- # Possible parameters:
116
- # * :user_agent - overrides the default user agent.
117
- # * :compress - any value to enable compression
118
- # * :http_authentication - array containing http authentication parameters
119
- # * :proxy_url - proxy url
120
- # * :proxy_port - proxy port
121
- # * :max_redirects - max number of redirections
122
- # * :timeout - timeout
123
- def self.setup_easy curl, options
124
- curl.headers["Accept-encoding"] = 'gzip, deflate' if options.has_key?(:compress)
125
- curl.headers["User-Agent"] = (options[:user_agent] || USER_AGENT)
126
-
127
- curl.userpwd = options[:http_authentication].join(':') if options.has_key?(:http_authentication)
128
- curl.proxy_url = options[:proxy_url] if options.has_key?(:proxy_url)
129
- curl.proxy_port = options[:proxy_port] if options.has_key?(:proxy_port)
130
- curl.max_redirects = options[:max_redirects] if options[:max_redirects]
131
- curl.timeout = options[:timeout] if options[:timeout]
132
-
133
- curl.follow_location = true
134
- end
135
-
136
- # Fetches and returns the raw XML for each URL provided.
137
- #
138
- # === Parameters
139
- # [urls<String> or <Array>] A single feed URL, or an array of feed URLs.
140
- # [options<Hash>] Valid keys for this argument as as followed:
141
- # :if_modified_since - Time object representing when the feed was last updated.
142
- # :if_none_match - String that's normally an etag for the request that was stored previously.
143
- # :on_success - Block that gets executed after a successful request.
144
- # :on_failure - Block that gets executed after a failed request.
145
- # * all parameters defined in setup_easy
146
- # === Returns
147
- # A String of XML if a single URL is passed.
148
- #
149
- # A Hash if multiple URL's are passed. The key will be the URL, and the value the XML.
150
- def self.fetch_raw(urls, options = {})
151
- url_queue = [*urls]
152
- multi = Curl::Multi.new
153
- responses = {}
154
- url_queue.each do |url|
155
- easy = Curl::Easy.new(url) do |curl|
156
- setup_easy curl, options
157
-
158
- curl.headers["If-Modified-Since"] = options[:if_modified_since].httpdate if options.has_key?(:if_modified_since)
159
- curl.headers["If-None-Match"] = options[:if_none_match] if options.has_key?(:if_none_match)
160
-
161
- curl.on_success do |c|
162
- responses[url] = decode_content(c)
163
- end
164
- curl.on_failure do |c, err|
165
- responses[url] = c.response_code
166
- end
167
- end
168
- multi.add(easy)
169
- end
170
-
171
- multi.perform
172
- urls.is_a?(String) ? responses.values.first : responses
173
- end
174
-
175
- # Fetches and returns the parsed XML for each URL provided.
176
- #
177
- # === Parameters
178
- # [urls<String> or <Array>] A single feed URL, or an array of feed URLs.
179
- # [options<Hash>] Valid keys for this argument as as followed:
180
- # * :user_agent - String that overrides the default user agent.
181
- # * :if_modified_since - Time object representing when the feed was last updated.
182
- # * :if_none_match - String, an etag for the request that was stored previously.
183
- # * :on_success - Block that gets executed after a successful request.
184
- # * :on_failure - Block that gets executed after a failed request.
185
- # === Returns
186
- # A Feed object if a single URL is passed.
187
- #
188
- # A Hash if multiple URL's are passed. The key will be the URL, and the value the Feed object.
189
- def self.fetch_and_parse(urls, options = {})
190
- url_queue = [*urls]
191
- multi = Curl::Multi.new
192
- responses = {}
193
-
194
- # I broke these down so I would only try to do 30 simultaneously because
195
- # I was getting weird errors when doing a lot. As one finishes it pops another off the queue.
196
- url_queue.slice!(0, 30).each do |url|
197
- add_url_to_multi(multi, url, url_queue, responses, options)
198
- end
199
-
200
- multi.perform
201
- return urls.is_a?(String) ? responses.values.first : responses
202
- end
203
-
204
- # Decodes the XML document if it was compressed.
205
- #
206
- # === Parameters
207
- # [curl_request<Curl::Easy>] The Curl::Easy response object from the request.
208
- # === Returns
209
- # A decoded string of XML.
210
- def self.decode_content(c)
211
- if c.header_str.match(/Content-Encoding: gzip/i)
212
- begin
213
- gz = Zlib::GzipReader.new(StringIO.new(c.body_str))
214
- xml = gz.read
215
- gz.close
216
- rescue Zlib::GzipFile::Error
217
- # Maybe this is not gzipped?
218
- xml = c.body_str
219
- end
220
- elsif c.header_str.match(/Content-Encoding: deflate/i)
221
- xml = Zlib::Inflate.inflate(c.body_str)
222
- else
223
- xml = c.body_str
224
- end
225
-
226
- xml
227
- end
228
-
229
- # Updates each feed for each Feed object provided.
230
- #
231
- # === Parameters
232
- # [feeds<Feed> or <Array>] A single feed object, or an array of feed objects.
233
- # [options<Hash>] Valid keys for this argument as as followed:
234
- # * :on_success - Block that gets executed after a successful request.
235
- # * :on_failure - Block that gets executed after a failed request.
236
- # * all parameters defined in setup_easy
237
- # === Returns
238
- # A updated Feed object if a single URL is passed.
239
- #
240
- # A Hash if multiple Feeds are passed. The key will be the URL, and the value the updated Feed object.
241
- def self.update(feeds, options = {})
242
- feed_queue = [*feeds]
243
- multi = Curl::Multi.new
244
- responses = {}
245
-
246
- feed_queue.slice!(0, 30).each do |feed|
247
- add_feed_to_multi(multi, feed, feed_queue, responses, options)
248
- end
249
-
250
- multi.perform
251
- responses.is_a?(Array)? responses.values : responses.values.first
252
- end
253
-
254
- # An abstraction for adding a feed by URL to the passed Curb::multi stack.
255
- #
256
- # === Parameters
257
- # [multi<Curl::Multi>] The Curl::Multi object that the request should be added too.
258
- # [url<String>] The URL of the feed that you would like to be fetched.
259
- # [url_queue<Array>] An array of URLs that are queued for request.
260
- # [responses<Hash>] Existing responses that you want the response from the request added to.
261
- # [feeds<String> or <Array>] A single feed object, or an array of feed objects.
262
- # [options<Hash>] Valid keys for this argument as as followed:
263
- # * :on_success - Block that gets executed after a successful request.
264
- # * :on_failure - Block that gets executed after a failed request.
265
- # * all parameters defined in setup_easy
266
- # === Returns
267
- # The updated Curl::Multi object with the request details added to it's stack.
268
- def self.add_url_to_multi(multi, url, url_queue, responses, options)
269
- easy = Curl::Easy.new(url) do |curl|
270
- setup_easy curl, options
271
- curl.headers["If-Modified-Since"] = options[:if_modified_since].httpdate if options.has_key?(:if_modified_since)
272
- curl.headers["If-None-Match"] = options[:if_none_match] if options.has_key?(:if_none_match)
273
-
274
- curl.on_success do |c|
275
- add_url_to_multi(multi, url_queue.shift, url_queue, responses, options) unless url_queue.empty?
276
- xml = decode_content(c)
277
- klass = determine_feed_parser_for_xml(xml)
278
-
279
- if klass
280
- begin
281
- feed = klass.parse(xml, Proc.new{|message| puts "Error while parsing [#{url}] #{message}" })
282
- feed.feed_url = c.last_effective_url
283
- feed.etag = etag_from_header(c.header_str)
284
- feed.last_modified = last_modified_from_header(c.header_str)
285
- responses[url] = feed
286
- options[:on_success].call(url, feed) if options.has_key?(:on_success)
287
- rescue Exception => e
288
- options[:on_failure].call(url, c.response_code, c.header_str, c.body_str) if options.has_key?(:on_failure)
289
- end
290
- else
291
- # puts "Error determining parser for #{url} - #{c.last_effective_url}"
292
- # raise NoParserAvailable.new("no valid parser for content.") (this would unfortunately fail the whole 'multi', so it's not really usable)
293
- options[:on_failure].call(url, c.response_code, c.header_str, c.body_str) if options.has_key?(:on_failure)
294
- end
295
- end
296
-
297
- curl.on_failure do |c, err|
298
- add_url_to_multi(multi, url_queue.shift, url_queue, responses, options) unless url_queue.empty?
299
- responses[url] = c.response_code
300
- if c.response_code == 304 # it's not modified. this isn't an error condition
301
- options[:on_success].call(url, nil) if options.has_key?(:on_success)
302
- else
303
- options[:on_failure].call(url, c.response_code, c.header_str, c.body_str) if options.has_key?(:on_failure)
304
- end
305
- end
306
- end
307
- multi.add(easy)
308
- end
309
-
310
- # An abstraction for adding a feed by a Feed object to the passed Curb::multi stack.
311
- #
312
- # === Parameters
313
- # [multi<Curl::Multi>] The Curl::Multi object that the request should be added too.
314
- # [feed<Feed>] A feed object that you would like to be fetched.
315
- # [url_queue<Array>] An array of feed objects that are queued for request.
316
- # [responses<Hash>] Existing responses that you want the response from the request added to.
317
- # [feeds<String>] or <Array> A single feed object, or an array of feed objects.
318
- # [options<Hash>] Valid keys for this argument as as followed:
319
- # * :on_success - Block that gets executed after a successful request.
320
- # * :on_failure - Block that gets executed after a failed request.
321
- # * all parameters defined in setup_easy
322
- # === Returns
323
- # The updated Curl::Multi object with the request details added to it's stack.
324
- def self.add_feed_to_multi(multi, feed, feed_queue, responses, options)
325
- easy = Curl::Easy.new(feed.feed_url) do |curl|
326
- setup_easy curl, options
327
- curl.headers["If-Modified-Since"] = feed.last_modified.httpdate if feed.last_modified
328
- curl.headers["If-Modified-Since"] = options[:if_modified_since] if options[:if_modified_since] && (!feed.last_modified || (Time.parse(options[:if_modified_since].to_s) > feed.last_modified))
329
- curl.headers["If-None-Match"] = feed.etag if feed.etag
330
-
331
- curl.on_success do |c|
332
- begin
333
- add_feed_to_multi(multi, feed_queue.shift, feed_queue, responses, options) unless feed_queue.empty?
334
- updated_feed = Feed.parse(c.body_str){ |message| puts "Error while parsing [#{feed.feed_url}] #{message}" }
335
- updated_feed.feed_url = c.last_effective_url
336
- updated_feed.etag = etag_from_header(c.header_str)
337
- updated_feed.last_modified = last_modified_from_header(c.header_str)
338
- feed.update_from_feed(updated_feed)
339
- responses[feed.feed_url] = feed
340
- options[:on_success].call(feed) if options.has_key?(:on_success)
341
- rescue Exception => e
342
- options[:on_failure].call(feed, c.response_code, c.header_str, c.body_str) if options.has_key?(:on_failure)
343
- end
344
- end
345
-
346
- curl.on_failure do |c, err|
347
- add_feed_to_multi(multi, feed_queue.shift, feed_queue, responses, options) unless feed_queue.empty?
348
- response_code = c.response_code
349
- if response_code == 304 # it's not modified. this isn't an error condition
350
- responses[feed.feed_url] = feed
351
- options[:on_success].call(feed) if options.has_key?(:on_success)
352
- else
353
- responses[feed.url] = c.response_code
354
- options[:on_failure].call(feed, c.response_code, c.header_str, c.body_str) if options.has_key?(:on_failure)
355
- end
356
- end
357
- end
358
- multi.add(easy)
359
- end
360
-
361
- # Determines the etag from the request headers.
362
- #
363
- # === Parameters
364
- # [header<String>] Raw request header returned from the request
365
- # === Returns
366
- # A string of the etag or nil if it cannot be found in the headers.
367
- def self.etag_from_header(header)
368
- header =~ /.*ETag:\s(.*)\r/
369
- $1
370
- end
371
-
372
- # Determines the last modified date from the request headers.
373
- #
374
- # === Parameters
375
- # [header<String>] Raw request header returned from the request
376
- # === Returns
377
- # A Time object of the last modified date or nil if it cannot be found in the headers.
378
- def self.last_modified_from_header(header)
379
- header =~ /.*Last-Modified:\s(.*)\r/
380
- Time.parse($1) if $1
381
- end
382
- end
383
- end