notu 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +5 -0
  3. data/.rspec +2 -0
  4. data/Gemfile +3 -0
  5. data/MIT-LICENSE +20 -0
  6. data/Rakefile +10 -0
  7. data/VERSION +1 -0
  8. data/lib/notu.rb +18 -0
  9. data/lib/notu/error.rb +15 -0
  10. data/lib/notu/html_document.rb +20 -0
  11. data/lib/notu/http_download.rb +39 -0
  12. data/lib/notu/library.rb +52 -0
  13. data/lib/notu/loved_tracks.rb +40 -0
  14. data/lib/notu/most_played_tracks.rb +46 -0
  15. data/lib/notu/network_error.rb +6 -0
  16. data/lib/notu/parse_error.rb +6 -0
  17. data/lib/notu/track.rb +30 -0
  18. data/notu.gemspec +27 -0
  19. data/spec/cassettes/Notu_HtmlDocument/_get/follows_redirects.yml +178 -0
  20. data/spec/cassettes/Notu_HtmlDocument/_get/raise_a_NetworkError_on_404.yml +80 -0
  21. data/spec/cassettes/Notu_HtmlDocument/_get/raise_a_ParseError_if_not_a_valid_document.yml +369 -0
  22. data/spec/cassettes/Notu_HtmlDocument/_get/returns_document_parsed.yml +134 -0
  23. data/spec/cassettes/Notu_HttpDownload/_get/accepts_HTTPS_URL.yml +134 -0
  24. data/spec/cassettes/Notu_HttpDownload/_get/follow_redirects.yml +178 -0
  25. data/spec/cassettes/Notu_HttpDownload/_get/raise_a_NetworkError_if_too_many_redirects.yml +47 -0
  26. data/spec/cassettes/Notu_HttpDownload/_get/raise_a_NetworkError_on_404.yml +80 -0
  27. data/spec/cassettes/Notu_HttpDownload/_get/retrives_document_from_given_URL.yml +134 -0
  28. data/spec/cassettes/Notu_LovedTracks/_each/returns_nil.yml +2568 -0
  29. data/spec/cassettes/Notu_LovedTracks/_each/returns_some_tracks.yml +5113 -0
  30. data/spec/cassettes/Notu_LovedTracks/_page_urls/is_correct.yml +5088 -0
  31. data/spec/cassettes/Notu_LovedTracks/_pages_count/is_correct.yml +2546 -0
  32. data/spec/cassettes/Notu_MostPlayedTracks/_each/returns_nil.yml +6532 -0
  33. data/spec/cassettes/Notu_MostPlayedTracks/_each/returns_some_tracks.yml +13455 -0
  34. data/spec/notu/error_spec.rb +40 -0
  35. data/spec/notu/html_document_spec.rb +31 -0
  36. data/spec/notu/http_download_spec.rb +39 -0
  37. data/spec/notu/library_spec.rb +130 -0
  38. data/spec/notu/loved_tracks_spec.rb +61 -0
  39. data/spec/notu/most_played_tracks_spec.rb +64 -0
  40. data/spec/notu/network_error_spec.rb +15 -0
  41. data/spec/notu/parse_error_spec.rb +15 -0
  42. data/spec/notu/track_spec.rb +67 -0
  43. data/spec/spec_helper.rb +10 -0
  44. data/spec/support/vcr.rb +8 -0
  45. metadata +252 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e624e1e0724b52253ee45953acadf9f0614dcbb3
4
+ data.tar.gz: 7540301e160396bf7630cb8398ea29f5a7959917
5
+ SHA512:
6
+ metadata.gz: ac2cef63ae77bd529389076774f156855ede52dd5d2bdd4e85ffd7a8bfce1a7b6ec0319b14cc3f18cbde4a7f346fe43b1682813bcefeacf4ce3a4e3767544051
7
+ data.tar.gz: 2d272e01ccfcecfafabc99b09c17155b1861346cc60004ed028ee3efdc8734f109050c16def7540d9ff8be07c61968f0a23dfbe88befe5e441cbe731e9aa00a8
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ .bundle
3
+ .ruby-version
4
+ Gemfile.lock
5
+ pkg/*
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --colour
2
+ --order random
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Alexis Toulotte
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ require 'rspec/core/rake_task'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ desc 'Default: runs specs.'
7
+ task default: :spec
8
+
9
+ desc 'Run all specs in spec directory.'
10
+ RSpec::Core::RakeTask.new(:spec)
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
data/lib/notu.rb ADDED
@@ -0,0 +1,18 @@
1
+ require 'active_support/core_ext'
2
+ require 'byebug' if ENV['DEBUGGER']
3
+ require 'cgi'
4
+ require 'net/https'
5
+ require 'nokogiri'
6
+
7
+ lib_path = "#{__dir__}/notu"
8
+
9
+ require "#{lib_path}/error"
10
+ require "#{lib_path}/network_error"
11
+ require "#{lib_path}/parse_error"
12
+
13
+ require "#{lib_path}/html_document"
14
+ require "#{lib_path}/http_download"
15
+ require "#{lib_path}/library"
16
+ require "#{lib_path}/loved_tracks"
17
+ require "#{lib_path}/most_played_tracks"
18
+ require "#{lib_path}/track"
data/lib/notu/error.rb ADDED
@@ -0,0 +1,15 @@
1
+ module Notu
2
+
3
+ class Error < StandardError
4
+
5
+ attr_reader :original
6
+
7
+ def initialize(message)
8
+ @original = message.is_a?(Exception) ? message : nil
9
+ message = original.message if original.is_a?(Exception)
10
+ super(message.to_s.squish.presence)
11
+ end
12
+
13
+ end
14
+
15
+ end
@@ -0,0 +1,20 @@
1
+ module Notu
2
+
3
+ module HtmlDocument
4
+
5
+ def self.get(url, options = {})
6
+ parse(HttpDownload.get(url, options))
7
+ end
8
+
9
+ private
10
+
11
+ def self.parse(data)
12
+ data = data.gsub(/&nbsp;/i, ' ').gsub(/\s+/, ' ')
13
+ document = Nokogiri::HTML.parse(data, nil, 'UTF-8')
14
+ raise ParseError.new('Invalid HTML document') if (document/'head').empty?
15
+ document
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,39 @@
1
+ module Notu
2
+
3
+ module HttpDownload
4
+
5
+ def self.get(url, options = {})
6
+ uri = url.is_a?(URI) ? url : URI.parse(url)
7
+ raise "Invalid URL: #{url.inspect}" unless uri.is_a?(URI::HTTP)
8
+ options.reverse_merge!(max_redirects: 10, timeout: 10, max_retries: 3, retry_sleep: 2)
9
+ connection = Net::HTTP.new(uri.host, uri.port)
10
+ connection.open_timeout = options[:timeout]
11
+ connection.read_timeout = options[:timeout]
12
+ if uri.is_a?(URI::HTTPS)
13
+ connection.use_ssl = true
14
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
15
+ end
16
+ connection.start do |http|
17
+ http.request_get(uri.request_uri, (options[:headers] || {}).stringify_keys) do |response|
18
+ begin
19
+ response.value # raise if not success
20
+ rescue Net::HTTPRetriableError
21
+ raise 'Max redirects has been reached' if options[:max_redirects] < 1
22
+ options[:max_redirects] -= 1
23
+ return get(response['Location'], options)
24
+ end
25
+ return response.body
26
+ end
27
+ end
28
+ rescue Net::ReadTimeout, TimeoutError, Timeout::Error, Zlib::BufError => exception
29
+ raise NetworkError.new(exception) if options[:max_retries] < 1
30
+ options[:max_retries] -= 1
31
+ sleep(options[:retry_sleep])
32
+ get(url, options)
33
+ rescue => exception
34
+ raise NetworkError.new(exception)
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,52 @@
1
+ module Notu
2
+
3
+ class Library
4
+
5
+ DEFAULT_HOST = 'www.last.fm'
6
+
7
+ attr_reader :host, :username
8
+
9
+ def initialize(options = {})
10
+ options = options.stringify_keys
11
+ self.host = options['host']
12
+ self.username = options['username']
13
+ end
14
+
15
+ def loved_tracks
16
+ LovedTracks.new(self)
17
+ end
18
+
19
+ def most_played_tracks(options = {})
20
+ MostPlayedTracks.new(self, options)
21
+ end
22
+
23
+ def url(options = {})
24
+ options = options.stringify_keys
25
+ path = options['path'].presence
26
+ query = options['query'].presence
27
+ query = options['query'].map { |name, value| "#{CGI.escape(name.to_s)}=#{CGI.escape(value.to_s)}" }.join('&') if options['query'].is_a?(Hash)
28
+ "http://#{host}/user/#{username}".tap do |url|
29
+ if path.present?
30
+ url << '/' unless path.starts_with?('/')
31
+ url << path
32
+ end
33
+ if query.present?
34
+ url << '?' << query
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def host=(value)
42
+ @host = value.presence || DEFAULT_HOST
43
+ end
44
+
45
+ def username=(value)
46
+ @username = value.to_s.strip.downcase
47
+ raise Error.new("Invalid Last.fm username: #{value.inspect}") if username !~ /^[a-z0-9_]+$/
48
+ end
49
+
50
+ end
51
+
52
+ end
@@ -0,0 +1,40 @@
1
+ module Notu
2
+
3
+ class LovedTracks
4
+
5
+ include Enumerable
6
+
7
+ attr_reader :library
8
+
9
+ def initialize(library)
10
+ raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
11
+ @library = library
12
+ end
13
+
14
+ def each(&block)
15
+ return unless block_given?
16
+ page_urls.each do |url|
17
+ document = HtmlDocument.get(url)
18
+ (document/'#lovedTracks td.subjectCell').each do |element|
19
+ yield(Track.new(artist: (element/'a').first.text, title: (element/'a').last.text))
20
+ end
21
+ end
22
+ nil
23
+ end
24
+
25
+ private
26
+
27
+ def page_urls
28
+ @loved_pages ||= (1..pages_count).map do |index|
29
+ library.url(path: 'library/loved', query: { 'sortBy' => 'date', 'sortOrder' => 'desc', 'page' => index })
30
+ end
31
+ end
32
+
33
+ def pages_count
34
+ document = HtmlDocument.get(library.url(path: 'library/loved'))
35
+ [1, (document/'div.whittle-pagination a').map { |link| link.text.to_i }].flatten.max
36
+ end
37
+
38
+ end
39
+
40
+ end
@@ -0,0 +1,46 @@
1
+ module Notu
2
+
3
+ class MostPlayedTracks
4
+
5
+ include Enumerable
6
+
7
+ PERIODS = {
8
+ 'last_week' => 'week',
9
+ 'last_month' => '1month',
10
+ 'last_3_months' => '3month',
11
+ 'last_6_months' => '6month',
12
+ 'last_year' => 'year',
13
+ 'overall' => 'overall',
14
+ }
15
+
16
+ attr_reader :library, :period
17
+
18
+ def initialize(library, options = {})
19
+ raise ArgumentError.new("#{self.class}#library must be a library, #{library.inspect} given") unless library.is_a?(Library)
20
+ @library = library
21
+ options = options.stringify_keys.reverse_merge('period' => PERIODS.keys.first)
22
+ self.period = options['period']
23
+ end
24
+
25
+ def each(&block)
26
+ return unless block_given?
27
+ document = HtmlDocument.get(library.url(path: 'charts', query: { 'rangetype' => PERIODS[period], 'subtype' => 'tracks' }))
28
+ (document/'table.chart tbody tr').each do |element|
29
+ artist = (element/'td.subjectCell a').first.text
30
+ plays_count = (element/'td.chartbarCell a span').text.strip
31
+ title = (element/'td.subjectCell a').last.text
32
+ yield(Track.new(artist: artist, plays_count: plays_count, title: title))
33
+ end
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def period=(value)
40
+ raise ArgumentError.new("Notu::MostPlayedTracks#period is invalid: #{value.inspect}") unless PERIODS.key?(value.to_s)
41
+ @period = value.to_s
42
+ end
43
+
44
+ end
45
+
46
+ end
@@ -0,0 +1,6 @@
1
+ module Notu
2
+
3
+ class NetworkError < Error
4
+ end
5
+
6
+ end
@@ -0,0 +1,6 @@
1
+ module Notu
2
+
3
+ class ParseError < Error
4
+ end
5
+
6
+ end
data/lib/notu/track.rb ADDED
@@ -0,0 +1,30 @@
1
+ module Notu
2
+
3
+ class Track
4
+
5
+ attr_reader :artist, :plays_count, :title
6
+
7
+ def initialize(attributes = {})
8
+ attributes = attributes.stringify_keys
9
+ self.artist = attributes['artist']
10
+ self.plays_count = attributes['plays_count']
11
+ self.title = attributes['title']
12
+ end
13
+
14
+ private
15
+
16
+ def artist=(value)
17
+ @artist = value.to_s.squish.presence || raise(Error.new("#{self.class}#artist must be specified, #{value.inspect} given"))
18
+ end
19
+
20
+ def plays_count=(value)
21
+ @plays_count = value.is_a?(Integer) || value.is_a?(String) && value =~ /\A[0-9]+\z/ ? [0, value.to_i].max : nil
22
+ end
23
+
24
+ def title=(value)
25
+ @title = value.to_s.squish.presence || raise(Error.new("#{self.class}#title must be specified, #{value.inspect} given"))
26
+ end
27
+
28
+ end
29
+
30
+ end
data/notu.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'notu'
3
+ s.version = File.read("#{File.dirname(__FILE__)}/VERSION").strip
4
+ s.platform = Gem::Platform::RUBY
5
+ s.author = 'Alexis Toulotte'
6
+ s.email = 'al@alweb.org'
7
+ s.homepage = 'https://github.com/alexistoulotte/notu'
8
+ s.summary = 'API for Last.fm'
9
+ s.description = 'API to get Last.fm most played and loved tracks'
10
+ s.license = 'MIT'
11
+
12
+ s.rubyforge_project = 'notu'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
17
+ s.require_paths = ['lib']
18
+
19
+ s.add_dependency 'activesupport', '>= 4.1.0', '< 4.2.0'
20
+ s.add_dependency 'nokogiri', '>= 1.6.0', '< 1.7.0'
21
+
22
+ s.add_development_dependency 'byebug', '>= 3.2.0', '< 3.3.0'
23
+ s.add_development_dependency 'rake', '>= 10.3.0', '< 10.4.0'
24
+ s.add_development_dependency 'rspec', '>= 3.0.0', '< 3.1.0'
25
+ s.add_development_dependency 'vcr', '>= 2.9.0', '< 2.10.0'
26
+ s.add_development_dependency 'webmock', '>= 1.18.0', '< 1.19.0'
27
+ end
@@ -0,0 +1,178 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: get
5
+ uri: http://www.alweb.org/
6
+ body:
7
+ encoding: US-ASCII
8
+ string: ''
9
+ headers:
10
+ Accept-Encoding:
11
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
12
+ Accept:
13
+ - "*/*"
14
+ User-Agent:
15
+ - Ruby
16
+ response:
17
+ status:
18
+ code: 301
19
+ message: Moved Permanently
20
+ headers:
21
+ Date:
22
+ - Thu, 28 Aug 2014 22:42:20 GMT
23
+ Server:
24
+ - Apache/2.2.16 (Debian)
25
+ Location:
26
+ - http://alweb.org/
27
+ Vary:
28
+ - Accept-Encoding
29
+ Content-Length:
30
+ - '241'
31
+ Content-Type:
32
+ - text/html; charset=iso-8859-1
33
+ body:
34
+ encoding: UTF-8
35
+ string: |
36
+ <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
37
+ <html><head>
38
+ <title>301 Moved Permanently</title>
39
+ </head><body>
40
+ <h1>Moved Permanently</h1>
41
+ <p>The document has moved <a href="http://alweb.org/">here</a>.</p>
42
+ <hr>
43
+ <address>Apache/2.2.16 (Debian) Server at www.alweb.org Port 80</address>
44
+ </body></html>
45
+ http_version:
46
+ recorded_at: Thu, 28 Aug 2014 22:42:21 GMT
47
+ - request:
48
+ method: get
49
+ uri: http://alweb.org/
50
+ body:
51
+ encoding: US-ASCII
52
+ string: ''
53
+ headers:
54
+ Accept-Encoding:
55
+ - gzip;q=1.0,deflate;q=0.6,identity;q=0.3
56
+ Accept:
57
+ - "*/*"
58
+ User-Agent:
59
+ - Ruby
60
+ response:
61
+ status:
62
+ code: 200
63
+ message: OK
64
+ headers:
65
+ Date:
66
+ - Thu, 28 Aug 2014 22:42:21 GMT
67
+ Server:
68
+ - Apache/2.2.16 (Debian)
69
+ X-Frame-Options:
70
+ - SAMEORIGIN
71
+ X-Xss-Protection:
72
+ - 1; mode=block
73
+ X-Content-Type-Options:
74
+ - nosniff
75
+ Etag:
76
+ - '"f24a7f595f7d88d135193e6f203bfbd5"'
77
+ Cache-Control:
78
+ - public
79
+ X-Request-Id:
80
+ - 005d51c9-30e8-45bc-886a-476b46509307
81
+ X-Runtime:
82
+ - '0.005857'
83
+ X-Powered-By:
84
+ - Phusion Passenger 4.0.49
85
+ Status:
86
+ - 200 OK
87
+ Vary:
88
+ - Accept-Encoding
89
+ Content-Length:
90
+ - '929'
91
+ Content-Type:
92
+ - text/html; charset=utf-8
93
+ body:
94
+ encoding: UTF-8
95
+ string: |
96
+ <!DOCTYPE html>
97
+
98
+ <html lang="fr">
99
+
100
+ <head>
101
+ <meta charset="utf-8" />
102
+ <title>Alexis Toulotte</title>
103
+ <meta name="author" content="Alexis Toulotte" />
104
+ <meta name="description" content="Page personnelle d'Alexis Toulotte." />
105
+ <meta name="robots" content="all" />
106
+ <link rel="schema.DC" href="http://purl.org/dc/elements/1.1/" />
107
+ <meta name="DC.format" content="text/html" />
108
+ <meta name="DC.language" content="fr" />
109
+ <meta name="DC.title" content="Alexis Toulotte" />
110
+ <meta name="DC.creator" content="Alexis Toulotte" />
111
+ <meta name="DC.description" content="Page personnelle d'Alexis Toulotte." />
112
+ <meta name="google-site-verification" content="1dodRvC0Zp-iAZilnTeT5PaY4bPKHwSzs9994-3g304" />
113
+ <script src="/assets/application-2faf062622c53593fc90ee0ba5667a48.js"></script>
114
+ <link href="/assets/application-a374b3c70bad2d98bffceac38d81f6d5.css" media="screen" rel="stylesheet" />
115
+ <link href="/avatar?size=128" rel="icon" type="image/png" />
116
+ <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Dancing Script" />
117
+ <link rel="stylesheet" href="//fonts.googleapis.com/css?family=Raleway" />
118
+ </head>
119
+
120
+ <body>
121
+
122
+ <h1>Alexis Toulotte</h1>
123
+
124
+ <ul class="services">
125
+ <li class="github">
126
+ <a href="https://github.com/alexistoulotte"><span>GitHub</span></a>
127
+ </li>
128
+ <li class="rubygems">
129
+ <a href="https://rubygems.org/profiles/alexistoulotte"><span>RubyGems</span></a>
130
+ </li>
131
+ <li class="cv">
132
+ <a href="https://dl.dropbox.com/u/5601946/CV.pdf"><span>CV</span></a>
133
+ </li>
134
+ <li class="email">
135
+ <a href="#"><span>Email</span></a>
136
+ </li>
137
+ <li class="phone">
138
+ <a href="callto://+687817612"><span>+687 81 76 12</span></a>
139
+ </li>
140
+ <li class="skype">
141
+ <a href="skype:alexistoulotte"><span>Skype</span></a>
142
+ </li>
143
+ <li class="twitter">
144
+ <a href="https://twitter.com/alexistoulotte"><span>Twitter</span></a>
145
+ </li>
146
+ <li class="facebook">
147
+ <a href="https://www.facebook.com/alexis.toulotte"><span>Facebook</span></a>
148
+ </li>
149
+ <li class="linked-in">
150
+ <a href="https://www.linkedin.com/in/alexistoulotte"><span>LinkedIn</span></a>
151
+ </li>
152
+ <li class="google-plus">
153
+ <a href="https://plus.google.com/111956742901852825014/posts"><span>Google+</span></a>
154
+ </li>
155
+ <li class="youtube">
156
+ <a href="https://www.youtube.com/alexistoulotte"><span>YouTube</span></a>
157
+ </li>
158
+ <li class="mixcloud">
159
+ <a href="https://www.mixcloud.com/alexistoulotte"><span>Mixcloud</span></a>
160
+ </li>
161
+ <li class="last-fm">
162
+ <a href="http://www.lastfm.fr/user/alexistoulotte"><span>Last.fm</span></a>
163
+ </li>
164
+ <li class="sound-cloud">
165
+ <a href="https://soundcloud.com/alexistoulotte"><span>SoundCloud</span></a>
166
+ </li>
167
+ <li class="delicious">
168
+ <a href="https://delicious.com/alexistoulotte"><span>Delicious</span></a>
169
+ </li>
170
+ </ul>
171
+
172
+
173
+ </body>
174
+
175
+ </html>
176
+ http_version:
177
+ recorded_at: Thu, 28 Aug 2014 22:42:21 GMT
178
+ recorded_with: VCR 2.9.2