metal_archives 3.0.1 → 3.1.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +59 -12
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +34 -20
  5. data/CHANGELOG.md +4 -0
  6. data/LICENSE.md +17 -4
  7. data/README.md +29 -14
  8. data/bin/console +8 -11
  9. data/config/inflections.rb +7 -0
  10. data/config/initializers/.keep +0 -0
  11. data/docker-compose.yml +10 -1
  12. data/lib/metal_archives.rb +56 -22
  13. data/lib/metal_archives/cache/base.rb +40 -0
  14. data/lib/metal_archives/cache/memory.rb +68 -0
  15. data/lib/metal_archives/cache/null.rb +22 -0
  16. data/lib/metal_archives/cache/redis.rb +49 -0
  17. data/lib/metal_archives/collection.rb +3 -5
  18. data/lib/metal_archives/configuration.rb +28 -21
  19. data/lib/metal_archives/errors.rb +9 -1
  20. data/lib/metal_archives/http_client.rb +42 -46
  21. data/lib/metal_archives/models/artist.rb +55 -26
  22. data/lib/metal_archives/models/band.rb +43 -36
  23. data/lib/metal_archives/models/{base_model.rb → base.rb} +53 -53
  24. data/lib/metal_archives/models/label.rb +7 -8
  25. data/lib/metal_archives/models/release.rb +11 -17
  26. data/lib/metal_archives/parsers/artist.rb +40 -35
  27. data/lib/metal_archives/parsers/band.rb +73 -29
  28. data/lib/metal_archives/parsers/base.rb +14 -0
  29. data/lib/metal_archives/parsers/country.rb +21 -0
  30. data/lib/metal_archives/parsers/date.rb +31 -0
  31. data/lib/metal_archives/parsers/genre.rb +67 -0
  32. data/lib/metal_archives/parsers/label.rb +21 -13
  33. data/lib/metal_archives/parsers/parser.rb +15 -77
  34. data/lib/metal_archives/parsers/release.rb +22 -18
  35. data/lib/metal_archives/parsers/year.rb +29 -0
  36. data/lib/metal_archives/version.rb +3 -3
  37. data/metal_archives.env.example +7 -4
  38. data/metal_archives.gemspec +7 -4
  39. data/nginx/default.conf +2 -2
  40. metadata +76 -32
  41. data/.github/workflows/release.yml +0 -69
  42. data/.rubocop_todo.yml +0 -92
  43. data/lib/metal_archives/lru_cache.rb +0 -61
  44. data/lib/metal_archives/middleware/cache_check.rb +0 -18
  45. data/lib/metal_archives/middleware/encoding.rb +0 -16
  46. data/lib/metal_archives/middleware/headers.rb +0 -38
  47. data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
  48. data/lib/metal_archives/nil_date.rb +0 -91
  49. data/lib/metal_archives/range.rb +0 -69
@@ -7,7 +7,7 @@ module MetalArchives
7
7
  ##
8
8
  # Represents a release
9
9
  #
10
- class Release < MetalArchives::BaseModel
10
+ class Release < Base
11
11
  ##
12
12
  # :attr_reader: id
13
13
  #
@@ -37,18 +37,18 @@ module MetalArchives
37
37
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
38
38
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
39
39
  #
40
- enum :type, values: %i(full_length live demo single ep video boxed_set split compilation split_video collaboration)
40
+ enum :type, values: [:full_length, :live, :demo, :single, :ep, :video, :boxed_set, :split, :compilation, :split_video, :collaboration]
41
41
 
42
42
  ##
43
43
  # :attr_reader: date_released
44
44
  #
45
- # Returns rdoc-ref:NilDate
45
+ # Returns rdoc-ref:Date
46
46
  #
47
47
  # [Raises]
48
48
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
49
49
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
50
50
  #
51
- property :date_released, type: NilDate
51
+ property :date_released, type: Date
52
52
 
53
53
  ##
54
54
  # :attr_reader_: catalog_id
@@ -124,12 +124,9 @@ module MetalArchives
124
124
  #
125
125
  def assemble # :nodoc:
126
126
  ## Base attributes
127
- url = "#{MetalArchives.config.default_endpoint}albums/view/id/#{id}"
128
- response = HTTPClient.get url
127
+ response = MetalArchives.http.get "/albums/view/id/#{id}"
129
128
 
130
- properties = Parsers::Release.parse_html response.body
131
-
132
- properties
129
+ Parsers::Release.parse_html response.to_s
133
130
  end
134
131
 
135
132
  class << self
@@ -142,7 +139,7 @@ module MetalArchives
142
139
  # +Integer+
143
140
  #
144
141
  def find(id)
145
- return cache[id] if cache.include? id
142
+ return MetalArchives.cache[id] if MetalArchives.cache.include? id
146
143
 
147
144
  Release.new id: id
148
145
  end
@@ -200,11 +197,10 @@ module MetalArchives
200
197
  # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
201
198
  #
202
199
  def find_by(query)
203
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/albums"
204
200
  params = Parsers::Release.map_params query
205
201
 
206
- response = HTTPClient.get url, params
207
- json = JSON.parse response.body
202
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params
203
+ json = JSON.parse response.to_s
208
204
 
209
205
  return nil if json["aaData"].empty?
210
206
 
@@ -286,8 +282,6 @@ module MetalArchives
286
282
  # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
287
283
  #
288
284
  def search_by(query)
289
- url = "#{MetalArchives.config.default_endpoint}search/ajax-advanced/searching/albums"
290
-
291
285
  params = Parsers::Release.map_params query
292
286
 
293
287
  l = lambda do
@@ -296,8 +290,8 @@ module MetalArchives
296
290
  if @max_items && @start >= @max_items
297
291
  []
298
292
  else
299
- response = HTTPClient.get url, params.merge(iDisplayStart: @start)
300
- json = JSON.parse response.body
293
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params.merge(iDisplayStart: @start)
294
+ json = JSON.parse response.to_s
301
295
 
302
296
  @max_items = json["iTotalRecords"]
303
297
 
@@ -20,11 +20,9 @@ module MetalArchives
20
20
  # +Hash+
21
21
  #
22
22
  def map_params(query)
23
- params = {
23
+ {
24
24
  query: query[:name] || "",
25
25
  }
26
-
27
- params
28
26
  end
29
27
 
30
28
  ##
@@ -36,13 +34,30 @@ module MetalArchives
36
34
  # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
37
35
  #
38
36
  def parse_html(response)
39
- props = {}
37
+ # Set default props
38
+ props = {
39
+ name: nil,
40
+ aliases: [],
41
+
42
+ date_of_birth: nil,
43
+ date_of_death: nil,
44
+ cause_of_death: nil,
45
+ gender: nil,
46
+
47
+ country: nil,
48
+ location: nil,
49
+
50
+ photo: nil,
51
+
52
+ bands: [],
53
+ }
54
+
40
55
  doc = Nokogiri::HTML response
41
56
 
42
57
  # Photo
43
58
  unless doc.css(".member_img").empty?
44
59
  photo_uri = URI doc.css(".member_img img").first.attr("src")
45
- props[:photo] = Middleware::RewriteEndpoint.rewrite photo_uri
60
+ props[:photo] = rewrite(photo_uri)
46
61
  end
47
62
 
48
63
  doc.css("#member_info dl").each do |dl|
@@ -55,25 +70,14 @@ module MetalArchives
55
70
  when "Real/full name:"
56
71
  props[:name] = content
57
72
  when "Age:"
58
- date = content.strip.gsub(/[0-9]* *\(born ([^\)]*)\)/, '\1')
59
- begin
60
- props[:date_of_birth] = NilDate.parse date
61
- rescue MetalArchives::Errors::ArgumentError => e
62
- dob = Date.parse date
63
- props[:date_of_birth] = NilDate.new dob.year, dob.month, dob.day
64
- end
73
+ props[:date_of_birth] = Parsers::Date.parse(content.strip.gsub(/[0-9]* *\(born ([^)]*)\)/, '\1'))
65
74
  when "R.I.P.:"
66
- begin
67
- dod = Date.parse content
68
- props[:date_of_death] = NilDate.new dod.year, dod.month, dod.day
69
- rescue ArgumentError => e
70
- props[:date_of_death] = NilDate.parse content
71
- end
75
+ props[:date_of_death] = Parsers::Date.parse(content)
72
76
  when "Died of:"
73
77
  props[:cause_of_death] = content
74
78
  when "Place of origin:"
75
- props[:country] = ISO3166::Country.find_country_by_name(sanitize(dt.next_element.css("a").first.content))
76
- location = dt.next_element.xpath("text()").map(&:content).join("").strip.gsub(/[()]/, "")
79
+ props[:country] = Country.parse(sanitize(dt.next_element.css("a").first.content))
80
+ location = dt.next_element.xpath("text()").map(&:content).join.strip.gsub(/[()]/, "")
77
81
  props[:location] = location unless location.empty?
78
82
  when "Gender:"
79
83
  case content
@@ -91,33 +95,34 @@ module MetalArchives
91
95
  end
92
96
 
93
97
  # Aliases
94
- props[:aliases] = []
95
98
  alt = sanitize doc.css(".band_member_name").first.content
96
99
  props[:aliases] << alt unless props[:name] == alt
97
100
 
98
101
  # Active bands
99
- props[:bands] = []
100
-
101
102
  proc = proc do |row|
102
103
  link = row.css("h3 a")
103
- band = if link.any?
104
- # Band name contains a link
105
- MetalArchives::Band.find Integer(link.attr("href").text.gsub(%r(^.*/([^/#]*)#.*$), '\1'))
106
- else
107
- # Band name does not contain a link
108
- sanitize row.css("h3").text
109
- end
104
+
105
+ name, id = nil
106
+
107
+ if link.any?
108
+ # Band name contains a link
109
+ id = Integer(link.attr("href").text.gsub(%r(^.*/([^/#]*)#.*$), '\1'))
110
+ else
111
+ # Band name does not contain a link
112
+ name = sanitize row.css("h3").text
113
+ end
110
114
 
111
115
  r = row.css(".member_in_band_role")
112
116
 
113
- range = parse_year_range r.xpath("text()").map(&:content).join("").strip.gsub(/[\n\r\t]/, "").gsub(/.*\((.*)\)/, '\1')
117
+ range = Parsers::Year.parse(r.xpath("text()").map(&:content).join.strip.gsub(/[\n\r\t]/, "").gsub(/.*\((.*)\)/, '\1'))
114
118
  role = sanitize r.css("strong").first.content
115
119
 
116
120
  {
117
- band: band,
118
- date_active: range,
121
+ id: id,
122
+ name: name,
123
+ years_active: range,
119
124
  role: role,
120
- }
125
+ }.compact
121
126
  end
122
127
 
123
128
  doc.css("#artist_tab_active .member_in_band").each do |row|
@@ -151,7 +156,7 @@ module MetalArchives
151
156
  type = :official
152
157
 
153
158
  doc.css("#linksTablemain tr").each do |row|
154
- if row["id"].match /^header_/
159
+ if /^header_/.match?(row["id"])
155
160
  type = row["id"].gsub(/^header_/, "").downcase.to_sym
156
161
  else
157
162
  a = row.css("td a").first
@@ -24,8 +24,8 @@ module MetalArchives
24
24
  bandName: query[:name] || "",
25
25
  exactBandMatch: (query[:exact] ? 1 : 0),
26
26
  genre: query[:genre] || "",
27
- yearCreationFrom: (query[:year]&.begin ? query[:year].begin.year : "") || "",
28
- yearCreationTo: (query[:year]&.end ? query[:year].end.year : "") || "",
27
+ yearCreationFrom: query[:year]&.begin || "",
28
+ yearCreationTo: query[:year]&.end || "",
29
29
  bandNotes: query[:comment] || "",
30
30
  status: map_status(query[:status]),
31
31
  themes: query[:lyrical_themes] || "",
@@ -52,23 +52,42 @@ module MetalArchives
52
52
  # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
53
53
  #
54
54
  def parse_html(response)
55
- props = {}
55
+ # Set default props
56
+ props = {
57
+ name: nil,
58
+ aliases: [],
59
+
60
+ logo: nil,
61
+ photo: nil,
62
+
63
+ country: nil,
64
+ location: nil,
65
+
66
+ status: nil,
67
+ date_formed: nil,
68
+ years_active: [],
69
+ independent: nil,
70
+
71
+ genres: [],
72
+ lyrical_themes: [],
73
+
74
+ members: [],
75
+ }
76
+
56
77
  doc = Nokogiri::HTML response
57
78
 
58
79
  props[:name] = sanitize doc.css("#band_info .band_name a").first.content
59
80
 
60
- props[:aliases] = []
61
-
62
81
  # Logo
63
82
  unless doc.css(".band_name_img").empty?
64
83
  logo_uri = URI doc.css(".band_name_img img").first.attr("src")
65
- props[:logo] = Middleware::RewriteEndpoint.rewrite logo_uri
84
+ props[:logo] = rewrite(logo_uri)
66
85
  end
67
86
 
68
87
  # Photo
69
88
  unless doc.css(".band_img").empty?
70
89
  photo_uri = URI doc.css(".band_img img").first.attr("src")
71
- props[:photo] = Middleware::RewriteEndpoint.rewrite photo_uri
90
+ props[:photo] = rewrite(photo_uri)
72
91
  end
73
92
 
74
93
  doc.css("#band_stats dl").each do |dl|
@@ -79,22 +98,16 @@ module MetalArchives
79
98
 
80
99
  case dt.content
81
100
  when "Country of origin:"
82
- props[:country] = ISO3166::Country.find_country_by_name sanitize(dt.next_element.css("a").first.content)
101
+ props[:country] = Country.parse(sanitize(dt.next_element.css("a").first.content))
83
102
  when "Location:"
84
103
  props[:location] = content
85
104
  when "Status:"
86
- props[:status] = content.downcase.tr(" ", "_").to_sym
105
+ props[:status] = content.downcase.tr(" -", "_").to_sym
87
106
  when "Formed in:"
88
- begin
89
- dof = Date.parse content
90
- props[:date_formed] = NilDate.new dof.year, dof.month, dof.day
91
- rescue ArgumentError => e
92
- props[:date_formed] = NilDate.parse content
93
- end
107
+ props[:date_formed] = Parsers::Date.parse(content)
94
108
  when "Genre:"
95
- props[:genres] = parse_genre content
109
+ props[:genres] = Parsers::Genre.parse(content)
96
110
  when "Lyrical themes:"
97
- props[:lyrical_themes] = []
98
111
  content.split(",").each do |theme|
99
112
  t = theme.split.map(&:capitalize)
100
113
  t.delete "(early)"
@@ -105,26 +118,56 @@ module MetalArchives
105
118
  props[:independent] = (content == "Unsigned/independent")
106
119
  # TODO: label
107
120
  when "Years active:"
108
- props[:date_active] = []
109
121
  content.split(",").each do |range|
110
122
  # Aliases
111
123
  range.scan(/\(as ([^)]*)\)/).each { |name| props[:aliases] << name.first }
112
124
  # Ranges
113
- r = range.gsub(/ *\(as ([^)]*)\) */, "").strip.split("-")
114
- date_start = (r.first == "?" ? nil : NilDate.new(r.first.to_i))
115
- date_end = (r.last == "?" || r.last == "present" ? nil : NilDate.new(r.first.to_i))
116
- props[:date_active] << MetalArchives::Range.new(date_start, date_end)
125
+ props[:years_active] << Parsers::Year.parse(range.gsub(/ *\(as ([^)]*)\) */, ""))
117
126
  end
118
127
  else
119
- raise MetalArchives::Errors::ParserError, "Unknown token: #{dt.content}"
128
+ raise Errors::ParserError, "Unknown token: #{dt.content}"
120
129
  end
121
130
  end
122
131
  end
123
132
 
133
+ # Members
134
+ proc = proc do |row|
135
+ link = row.css("a")
136
+
137
+ if link.any?
138
+ # Artist name contains a link
139
+ id = Integer(link.attr("href").text.split("/").last)
140
+ name = sanitize link.text
141
+ else
142
+ # Artist name does not contain a link
143
+ name = sanitize row.css("h3").text
144
+ end
145
+
146
+ r = row.css("td").last.text
147
+ role, range = r.match(/(.*)\(([^(]*)\)/).captures
148
+
149
+ range = Parsers::Year.parse(range)
150
+
151
+ {
152
+ id: id,
153
+ name: name,
154
+ years_active: range,
155
+ role: sanitize(role),
156
+ }.compact
157
+ end
158
+
159
+ doc.css("#band_tab_members_current .lineupRow").each do |row|
160
+ props[:members] << proc.call(row).merge(current: true)
161
+ end
162
+
163
+ doc.css("#band_tab_members_past .lineupRow").each do |row|
164
+ props[:members] << proc.call(row).merge(current: false)
165
+ end
166
+
124
167
  props
125
168
  rescue StandardError => e
126
169
  e.backtrace.each { |b| MetalArchives.config.logger.error b }
127
- raise MetalArchives::Errors::ParserError, e
170
+ raise Errors::ParserError, e
128
171
  end
129
172
 
130
173
  ##
@@ -141,15 +184,16 @@ module MetalArchives
141
184
  doc = Nokogiri::HTML response
142
185
  doc.css("#artist_list tbody tr").each do |row|
143
186
  similar << {
144
- band: MetalArchives::Band.new(id: row.css("td a").first["href"].split("/").last.to_i),
187
+ id: row.css("td a").first["href"].split("/").last.to_i,
145
188
  score: row.css("td").last.content.strip,
146
189
  }
147
190
  end
148
191
 
149
192
  similar
150
193
  rescue StandardError => e
194
+ MetalArchives.config.logger e.message
151
195
  e.backtrace.each { |b| MetalArchives.config.logger.error b }
152
- raise MetalArchives::Errors::ParserError, e
196
+ raise Errors::ParserError, e
153
197
  end
154
198
 
155
199
  ##
@@ -182,7 +226,7 @@ module MetalArchives
182
226
  links
183
227
  rescue StandardError => e
184
228
  e.backtrace.each { |b| MetalArchives.config.logger.error b }
185
- raise MetalArchives::Errors::ParserError, e
229
+ raise Errors::ParserError, e
186
230
  end
187
231
 
188
232
  ##
@@ -204,7 +248,7 @@ module MetalArchives
204
248
  releases
205
249
  rescue StandardError => e
206
250
  e.backtrace.each { |b| MetalArchives.config.logger.error b }
207
- raise MetalArchives::Errors::ParserError, e
251
+ raise Errors::ParserError, e
208
252
  end
209
253
 
210
254
  private
@@ -228,7 +272,7 @@ module MetalArchives
228
272
  :disputed => "Disputed",
229
273
  }
230
274
 
231
- raise MetalArchives::Errors::ParserError, "Unknown status: #{status}" unless s[status]
275
+ raise Errors::ParserError, "Unknown status: #{status}" unless s[status]
232
276
 
233
277
  s[status]
234
278
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Parsers
5
+ ##
6
+ # Abstract base class
7
+ #
8
+ class Base
9
+ def self.parse(_input)
10
+ raise Errors::NotImplementedError, "method .parse not implemented"
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "countries"
4
+
5
+ module MetalArchives
6
+ module Parsers
7
+ ##
8
+ # Country parser
9
+ #
10
+ class Country < Base
11
+ ##
12
+ # Parse a country
13
+ #
14
+ # Returns +ISO3166::Country+
15
+ #
16
+ def self.parse(input)
17
+ ISO3166::Country.find_country_by_name(input)
18
+ end
19
+ end
20
+ end
21
+ end