metal_archives 0.8.0 → 1.0.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +59 -7
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +14 -0
  5. data/.travis.yml +11 -0
  6. data/Gemfile +2 -0
  7. data/{LICENSE → LICENSE.md} +0 -0
  8. data/README.md +77 -9
  9. data/Rakefile +5 -3
  10. data/lib/metal_archives.rb +8 -0
  11. data/lib/metal_archives/configuration.rb +28 -7
  12. data/lib/metal_archives/error.rb +37 -30
  13. data/lib/metal_archives/http_client.rb +21 -42
  14. data/lib/metal_archives/middleware/headers.rb +38 -0
  15. data/lib/metal_archives/middleware/rewrite_endpoint.rb +38 -0
  16. data/lib/metal_archives/models/artist.rb +51 -65
  17. data/lib/metal_archives/models/band.rb +41 -39
  18. data/lib/metal_archives/models/base_model.rb +88 -59
  19. data/lib/metal_archives/models/label.rb +7 -6
  20. data/lib/metal_archives/parsers/artist.rb +110 -99
  21. data/lib/metal_archives/parsers/band.rb +168 -156
  22. data/lib/metal_archives/parsers/label.rb +54 -52
  23. data/lib/metal_archives/parsers/parser.rb +73 -71
  24. data/lib/metal_archives/utils/collection.rb +7 -1
  25. data/lib/metal_archives/utils/lru_cache.rb +11 -4
  26. data/lib/metal_archives/utils/nil_date.rb +54 -0
  27. data/lib/metal_archives/utils/range.rb +16 -8
  28. data/lib/metal_archives/version.rb +3 -1
  29. data/metal_archives.gemspec +21 -11
  30. data/spec/configuration_spec.rb +101 -0
  31. data/spec/factories/artist_factory.rb +37 -0
  32. data/spec/factories/band_factory.rb +60 -0
  33. data/spec/factories/nil_date_factory.rb +9 -0
  34. data/spec/factories/range_factory.rb +8 -0
  35. data/spec/models/artist_spec.rb +142 -0
  36. data/spec/models/band_spec.rb +179 -0
  37. data/spec/models/base_model_spec.rb +217 -0
  38. data/spec/parser_spec.rb +19 -0
  39. data/spec/spec_helper.rb +111 -0
  40. data/spec/support/factory_girl.rb +5 -0
  41. data/spec/support/metal_archives.rb +26 -0
  42. data/spec/utils/collection_spec.rb +72 -0
  43. data/spec/utils/lru_cache_spec.rb +53 -0
  44. data/spec/utils/nil_date_spec.rb +98 -0
  45. data/spec/utils/range_spec.rb +62 -0
  46. metadata +142 -57
  47. data/test/base_model_test.rb +0 -111
  48. data/test/configuration_test.rb +0 -57
  49. data/test/parser_test.rb +0 -37
  50. data/test/property/artist_property_test.rb +0 -43
  51. data/test/property/band_property_test.rb +0 -94
  52. data/test/query/artist_query_test.rb +0 -109
  53. data/test/query/band_query_test.rb +0 -152
  54. data/test/test_helper.rb +0 -25
  55. data/test/utils/collection_test.rb +0 -51
  56. data/test/utils/lru_cache_test.rb +0 -22
  57. data/test/utils/range_test.rb +0 -42
@@ -1,182 +1,194 @@
1
+ # frozen_string_literal: true
1
2
  require 'json'
2
3
  require 'date'
3
4
  require 'countries'
4
5
 
5
6
  module MetalArchives
6
- module Parsers
7
- ##
8
- # Band parser
9
- #
10
- class Band < Parser # :nodoc:
11
- class << self
12
- ##
13
- # Map attributes to MA attributes
14
- #
15
- # Returns +Hash+
16
- #
17
- # [+params+]
18
- # +Hash+
19
- #
20
- def map_params(query)
21
- params = {
22
- :bandName => query[:name] || '',
23
- :exactBandMatch => (!!query[:exact] ? 1 : 0),
24
- :genre => query[:genre] || '',
25
- :yearCreationFrom => (query[:year] and query[:year].begin ? query[:year].begin.year : '') || '',
26
- :yearCreationTo => (query[:year] and query[:year].end ? query[:year].end.year : '') || '',
27
- :bandNotes => query[:comment] || '',
28
- :status => map_status(query[:status]),
29
- :themes => query[:lyrical_themes] || '',
30
- :location => query[:location] || '',
31
- :bandLabelName => query[:label] || '',
32
- :indieLabelBand => (!!query[:independent] ? 1 : 0)
33
- }
34
-
35
- params[:country] = []
36
- Array(query[:country]).each do |country|
37
- params[:country] << (country.is_a?(ISO3166::Country) ? country.alpha2 : (country || ''))
7
+ module Parsers
8
+ ##
9
+ # Band parser
10
+ #
11
+ class Band < Parser # :nodoc:
12
+ class << self
13
+ ##
14
+ # Map attributes to MA attributes
15
+ #
16
+ # Returns +Hash+
17
+ #
18
+ # [+params+]
19
+ # +Hash+
20
+ #
21
+ def map_params(query)
22
+ params = {
23
+ :bandName => query[:name] || '',
24
+ :exactBandMatch => (!!query[:exact] ? 1 : 0),
25
+ :genre => query[:genre] || '',
26
+ :yearCreationFrom => (query[:year] && query[:year].begin ? query[:year].begin.year : '') || '',
27
+ :yearCreationTo => (query[:year] && query[:year].end ? query[:year].end.year : '') || '',
28
+ :bandNotes => query[:comment] || '',
29
+ :status => map_status(query[:status]),
30
+ :themes => query[:lyrical_themes] || '',
31
+ :location => query[:location] || '',
32
+ :bandLabelName => query[:label] || '',
33
+ :indieLabelBand => (!!query[:independent] ? 1 : 0)
34
+ }
35
+
36
+ params[:country] = []
37
+ Array(query[:country]).each do |country|
38
+ params[:country] << (country.is_a?(ISO3166::Country) ? country.alpha2 : (country || ''))
39
+ end
40
+ params[:country] = params[:country].first if params[:country].size == 1
41
+
42
+ params
38
43
  end
39
- params[:country] = params[:country].first if (params[:country].size == 1)
40
44
 
41
- params
42
- end
45
+ ##
46
+ # Parse main HTML page
47
+ #
48
+ # Returns +Hash+
49
+ #
50
+ # [Raises]
51
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
52
+ #
53
+ def parse_html(response)
54
+ props = {}
55
+ doc = Nokogiri::HTML response
43
56
 
44
- ##
45
- # Parse main HTML page
46
- #
47
- # Returns +Hash+
48
- #
49
- # [Raises]
50
- # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
51
- #
52
- def parse_html(response)
53
- props = {}
54
- doc = Nokogiri::HTML response
55
-
56
- props[:name] = sanitize doc.css('#band_info .band_name a').first.content
57
-
58
- props[:aliases] = []
59
- props[:logo] = doc.css('.band_name_img img').first.attr('src') unless doc.css('.band_name_img').empty?
60
- props[:photo] = doc.css('.band_img img').first.attr('src') unless doc.css('.band_img').empty?
61
-
62
- doc.css('#band_stats dl').each do |dl|
63
- dl.search('dt').each do |dt|
64
- content = sanitize(dt.next_element.content)
65
-
66
- next if content == 'N/A'
67
-
68
- case dt.content
69
- when 'Country of origin:'
70
- props[:country] = ISO3166::Country.find_country_by_name sanitize(dt.next_element.css('a').first.content)
71
- when 'Location:'
72
- props[:location] = content
73
- when 'Status:'
74
- props[:status] = content.downcase.gsub(/ /, '_').to_sym
75
- when 'Formed in:'
76
- props[:date_formed] = Date.new content.to_i
77
- when 'Genre:'
78
- props[:genres] = parse_genre content
79
- when 'Lyrical themes:'
80
- props[:lyrical_themes] = []
81
- content.split(',').each do |theme|
82
- t = theme.split.map(&:capitalize)
83
- t.delete '(early)'
84
- t.delete '(later)'
85
- props[:lyrical_themes] << t.join(' ')
86
- end
87
- when /(Current|Last) label:/
88
- props[:independent] = (content == 'Unsigned/independent')
89
- # TODO
90
- when 'Years active:'
91
- props[:date_active] = []
92
- content.split(',').each do |range|
93
- # Aliases
94
- range.scan(/\(as ([^)]*)\)/).each { |name| props[:aliases] << name.first }
95
- # Ranges
96
- r = range.gsub(/ *\(as ([^)]*)\) */, '').strip.split('-')
97
- date_start = (r.first == '?' ? nil : Date.new(r.first.to_i))
98
- date_end = (r.last == '?' or r.last == 'present' ? nil : Date.new(r.first.to_i))
99
- props[:date_active] << MetalArchives::Range.new(date_start, date_end)
57
+ props[:name] = sanitize doc.css('#band_info .band_name a').first.content
58
+
59
+ props[:aliases] = []
60
+
61
+ # Logo
62
+ unless doc.css('.band_name_img').empty?
63
+ logo_uri = URI doc.css('.band_name_img img').first.attr('src')
64
+ props[:logo] = Middleware::RewriteEndpoint.rewrite logo_uri
65
+ end
66
+
67
+ # Photo
68
+ unless doc.css('.band_img').empty?
69
+ photo_uri = URI doc.css('.band_img img').first.attr('src')
70
+ props[:photo] = Middleware::RewriteEndpoint.rewrite photo_uri
71
+ end
72
+
73
+ doc.css('#band_stats dl').each do |dl|
74
+ dl.search('dt').each do |dt|
75
+ content = sanitize(dt.next_element.content)
76
+
77
+ next if content == 'N/A'
78
+
79
+ case dt.content
80
+ when 'Country of origin:'
81
+ props[:country] = ISO3166::Country.find_country_by_name sanitize(dt.next_element.css('a').first.content)
82
+ when 'Location:'
83
+ props[:location] = content
84
+ when 'Status:'
85
+ props[:status] = content.downcase.tr(' ', '_').to_sym
86
+ when 'Formed in:'
87
+ props[:date_formed] = Date.new content.to_i
88
+ when 'Genre:'
89
+ props[:genres] = parse_genre content
90
+ when 'Lyrical themes:'
91
+ props[:lyrical_themes] = []
92
+ content.split(',').each do |theme|
93
+ t = theme.split.map(&:capitalize)
94
+ t.delete '(early)'
95
+ t.delete '(later)'
96
+ props[:lyrical_themes] << t.join(' ')
97
+ end
98
+ when /(Current|Last) label:/
99
+ props[:independent] = (content == 'Unsigned/independent')
100
+ # TODO
101
+ when 'Years active:'
102
+ props[:date_active] = []
103
+ content.split(',').each do |range|
104
+ # Aliases
105
+ range.scan(/\(as ([^)]*)\)/).each { |name| props[:aliases] << name.first }
106
+ # Ranges
107
+ r = range.gsub(/ *\(as ([^)]*)\) */, '').strip.split('-')
108
+ date_start = (r.first == '?' ? nil : Date.new(r.first.to_i))
109
+ date_end = (r.last ==( '?') || r.last == 'present' ? nil : Date.new(r.first.to_i))
110
+ props[:date_active] << MetalArchives::Range.new(date_start, date_end)
111
+ end
112
+ else
113
+ raise MetalArchives::Errors::ParserError, "Unknown token: #{dt.content}"
100
114
  end
101
- else
102
- raise MetalArchives::Errors::ParserError, "Unknown token: #{dt.content}"
103
115
  end
104
116
  end
117
+
118
+ props
119
+ rescue => e
120
+ e.backtrace.each { |b| MetalArchives.config.logger.error b }
121
+ raise Errors::ParserError, e
105
122
  end
106
123
 
107
- props
108
- rescue => e
109
- e.backtrace.each { |b| MetalArchives::config.logger.error b }
110
- raise Errors::ParserError, e
111
- end
124
+ ##
125
+ # Parse similar bands HTML page
126
+ #
127
+ # Returns +Hash+
128
+ #
129
+ # [Raises]
130
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
131
+ #
132
+ def parse_similar_bands_html(response)
133
+ similar = []
112
134
 
113
- ##
114
- # Parse similar bands HTML page
115
- #
116
- # Returns +Hash+
117
- #
118
- # [Raises]
119
- # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
120
- #
121
- def parse_similar_bands_html(response)
122
- similar = []
123
-
124
- doc = Nokogiri::HTML response
125
- doc.css('#artist_list tbody tr').each do |row|
126
- similar << {
135
+ doc = Nokogiri::HTML response
136
+ doc.css('#artist_list tbody tr').each do |row|
137
+ similar << {
127
138
  :band => MetalArchives::Band.new(:id => row.css('td a').first['href'].split('/').last.to_i),
128
- :score => row.css('td').last.content.strip
129
- }
130
- end
131
-
132
- similar
133
- rescue => e
134
- e.backtrace.each { |b| MetalArchives::config.logger.error b }
135
- raise Errors::ParserError, e
136
- end
139
+ :score => row.css('td').last.content.strip
140
+ }
141
+ end
137
142
 
138
- ##
139
- # Parse related links HTML page
140
- #
141
- # Returns +Hash+
142
- #
143
- # [Raises]
144
- # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
145
- #
146
- def parse_related_links_html(response)
147
- links = []
148
-
149
- doc = Nokogiri::HTML response
150
- doc.css('#linksTableOfficial td a').each do |a|
151
- links << {
152
- :url => a['href'],
153
- :type => :official,
154
- :title => a.content
155
- }
143
+ similar
144
+ rescue => e
145
+ e.backtrace.each { |b| MetalArchives.config.logger.error b }
146
+ raise Errors::ParserError, e
156
147
  end
157
- doc.css('#linksTableOfficial_merchandise td a').each do |a|
158
- links << {
159
- :url => a['href'],
160
- :type => :merchandise,
161
- :title => a.content
162
- }
163
- end
164
-
165
- links
166
- rescue => e
167
- e.backtrace.each { |b| MetalArchives::config.logger.error b }
168
- raise Errors::ParserError, e
169
- end
170
148
 
171
- private
172
149
  ##
173
- # Map MA band status
150
+ # Parse related links HTML page
174
151
  #
175
- # Returns +Symbol+
152
+ # Returns +Hash+
176
153
  #
177
154
  # [Raises]
178
155
  # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
179
156
  #
157
+ def parse_related_links_html(response)
158
+ links = []
159
+
160
+ doc = Nokogiri::HTML response
161
+ doc.css('#linksTableOfficial td a').each do |a|
162
+ links << {
163
+ :url => a['href'],
164
+ :type => :official,
165
+ :title => a.content
166
+ }
167
+ end
168
+ doc.css('#linksTableOfficial_merchandise td a').each do |a|
169
+ links << {
170
+ :url => a['href'],
171
+ :type => :merchandise,
172
+ :title => a.content
173
+ }
174
+ end
175
+
176
+ links
177
+ rescue => e
178
+ e.backtrace.each { |b| MetalArchives.config.logger.error b }
179
+ raise Errors::ParserError, e
180
+ end
181
+
182
+ private
183
+
184
+ ##
185
+ # Map MA band status
186
+ #
187
+ # Returns +Symbol+
188
+ #
189
+ # [Raises]
190
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
191
+ #
180
192
  def map_status(status)
181
193
  s = {
182
194
  nil => '',
@@ -192,7 +204,7 @@ module Parsers
192
204
 
193
205
  s[status]
194
206
  end
207
+ end
195
208
  end
196
209
  end
197
210
  end
198
- end
@@ -1,68 +1,70 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'nokogiri'
3
5
 
4
6
  module MetalArchives
5
- module Parsers
6
- ##
7
- # Label parser
8
- #
9
- class Label # :nodoc:
10
- class << self
11
- def find_endpoint(params)
12
- "#{MetalArchives.config.endpoint}labels/#{params[:name]}/#{params[:id]}"
13
- end
7
+ module Parsers
8
+ ##
9
+ # Label parser
10
+ #
11
+ class Label # :nodoc:
12
+ class << self
13
+ def find_endpoint(params)
14
+ "#{MetalArchives.config.default_endpoint}labels/#{params[:name]}/#{params[:id]}"
15
+ end
14
16
 
15
- def parse(response)
16
- props = {}
17
- doc = Nokogiri::HTML(response)
17
+ def parse(response)
18
+ props = {}
19
+ doc = Nokogiri::HTML(response)
18
20
 
19
- props[:name] = doc.css('#label_info .label_name').first.content
21
+ props[:name] = doc.css('#label_info .label_name').first.content
20
22
 
21
- props[:contact] = []
22
- doc.css('#label_contact a').each do |contact|
23
- props[:contact] << {
24
- :title => contact.content,
25
- :content => contact.attr(:href)
26
- }
27
- end
23
+ props[:contact] = []
24
+ doc.css('#label_contact a').each do |contact|
25
+ props[:contact] << {
26
+ :title => contact.content,
27
+ :content => contact.attr(:href)
28
+ }
29
+ end
28
30
 
29
- doc.css('#label_info dl').each do |dl|
30
- dl.search('dt').each do |dt|
31
- case dt.content
32
- when 'Address:'
33
- break if dt.next_element.content == 'N/A'
34
- props[:address] = dt.next_element.content
35
- when 'Country:'
36
- break if dt.next_element.content == 'N/A'
37
- props[:country] = ParserHelper.parse_country dt.next_element.css('a').first.content
38
- when 'Phone number:'
39
- break if dt.next_element.content == 'N/A'
40
- props[:phone] = dt.next_element.content
41
- when 'Status:'
42
- props[:status] = dt.next_element.content.downcase.gsub(/ /, '_').to_sym
43
- when 'Specialised in:'
44
- break if dt.next_element.content == 'N/A'
45
- props[:specializations] = ParserHelper.parse_genre dt.next_element.content
46
- when 'Founding date :'
47
- break if dt.next_element.content == 'N/A'
48
- props[:date_founded] = Date.new dt.next_element.content.to_i
49
- when 'Sub-labels:'
50
- # TODO
51
- when 'Online shopping:'
52
- if dt.next_element.content == 'Yes'
53
- props[:online_shopping] = true
54
- elsif dt.next_element.content == 'No'
55
- props[:online_shopping] = false
31
+ doc.css('#label_info dl').each do |dl|
32
+ dl.search('dt').each do |dt|
33
+ case dt.content
34
+ when 'Address:'
35
+ break if dt.next_element.content == 'N/A'
36
+ props[:address] = dt.next_element.content
37
+ when 'Country:'
38
+ break if dt.next_element.content == 'N/A'
39
+ props[:country] = ParserHelper.parse_country dt.next_element.css('a').first.content
40
+ when 'Phone number:'
41
+ break if dt.next_element.content == 'N/A'
42
+ props[:phone] = dt.next_element.content
43
+ when 'Status:'
44
+ props[:status] = dt.next_element.content.downcase.tr(' ', '_').to_sym
45
+ when 'Specialised in:'
46
+ break if dt.next_element.content == 'N/A'
47
+ props[:specializations] = ParserHelper.parse_genre dt.next_element.content
48
+ when 'Founding date :'
49
+ break if dt.next_element.content == 'N/A'
50
+ props[:date_founded] = Date.new dt.next_element.content.to_i
51
+ when 'Sub-labels:'
52
+ # TODO
53
+ when 'Online shopping:'
54
+ if dt.next_element.content == 'Yes'
55
+ props[:online_shopping] = true
56
+ elsif dt.next_element.content == 'No'
57
+ props[:online_shopping] = false
58
+ end
59
+ else
60
+ raise "Unknown token: #{dt.content}"
56
61
  end
57
- else
58
- raise "Unknown token: #{dt.content}"
59
62
  end
60
63
  end
61
- end
62
64
 
63
- props
65
+ props
66
+ end
64
67
  end
65
68
  end
66
69
  end
67
70
  end
68
- end