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
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Parsers
5
+ ##
6
+ # Date parser
7
+ #
8
+ class Date < Base
9
+ ##
10
+ # Parse a date
11
+ #
12
+ # Returns +Date+
13
+ #
14
+ def self.parse(input)
15
+ ::Date.parse(input)
16
+ rescue ::Date::Error
17
+ components = input
18
+ .split("-")
19
+ .map(&:to_i)
20
+ .reject(&:zero?)
21
+ .compact
22
+
23
+ return if components.empty?
24
+
25
+ ::Date.new(*components)
26
+ rescue TypeError
27
+ nil
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Parsers
5
+ ##
6
+ # Genre parser
7
+ #
8
+ class Genre < Base
9
+ SUFFIXES = %w((early) (later) metal).freeze
10
+
11
+ ##
12
+ # Opinionated parsing of genres
13
+ #
14
+ # Returns an +Array+ of +String+
15
+ #
16
+ # The following components are omitted:
17
+ # - Metal
18
+ # - (early)
19
+ # - (later)
20
+ #
21
+ # All genres are capitalized.
22
+ #
23
+ # For examples on how genres are parsed, refer to +gnre_spec.rb+
24
+ #
25
+ def self.parse(input)
26
+ genres = []
27
+ # Split fields
28
+ input.split(",").each do |genre|
29
+ ##
30
+ # Start with a single empty genre string. Split the genre by spaces
31
+ # and process each component. If a component does not have a slash,
32
+ # concatenate it to all genre strings present in +temp+. If it does
33
+ # have a slash present, duplicate all genre strings, and concatenate
34
+ # the first component (before the slash) to the first half, and the
35
+ # last component to the last half. +temp+ now has an array of genre
36
+ # combinations.
37
+ #
38
+ # 'Traditional Heavy/Power Metal' => ['Traditional Heavy', 'Traditional Power']
39
+ # 'Traditional/Classical Heavy/Power Metal' => [
40
+ # 'Traditional Heavy', 'Traditional Power',
41
+ # 'Classical Heavy', 'Classical Power']
42
+ #
43
+ temp = [""]
44
+
45
+ genre.downcase.split.reject { |g| SUFFIXES.include? g }.each do |g|
46
+ if g.include? "/"
47
+ # Duplicate all WIP genres
48
+ temp2 = temp.dup
49
+
50
+ # Assign first and last components to temp and temp2 respectively
51
+ split = g.split "/"
52
+ temp.map! { |t| t.empty? ? split.first.capitalize : "#{t.capitalize} #{split.first.capitalize}" }
53
+ temp2.map! { |t| t.empty? ? split.last.capitalize : "#{t.capitalize} #{split.last.capitalize}" }
54
+
55
+ # Add both genre trees
56
+ temp += temp2
57
+ else
58
+ temp.map! { |t| t.empty? ? g.capitalize : "#{t.capitalize} #{g.capitalize}" }
59
+ end
60
+ end
61
+ genres += temp
62
+ end
63
+ genres.uniq
64
+ end
65
+ end
66
+ end
67
+ end
@@ -11,16 +11,28 @@ module MetalArchives
11
11
  class Label # :nodoc:
12
12
  class << self
13
13
  def find_endpoint(params)
14
- "#{MetalArchives.config.default_endpoint}labels/#{params[:name]}/#{params[:id]}"
14
+ "#{MetalArchives.config.endpoint}labels/#{params[:name]}/#{params[:id]}"
15
15
  end
16
16
 
17
17
  def parse(response)
18
- props = {}
18
+ # Set default props
19
+ props = {
20
+ name: nil,
21
+ contact: [],
22
+ address: nil,
23
+ country: nil,
24
+ phone: nil,
25
+ status: nil,
26
+ specialization: [],
27
+ date_founded: nil,
28
+
29
+ online_shopping: nil,
30
+ }
31
+
19
32
  doc = Nokogiri::HTML(response)
20
33
 
21
34
  props[:name] = doc.css("#label_info .label_name").first.content
22
35
 
23
- props[:contact] = []
24
36
  doc.css("#label_contact a").each do |contact|
25
37
  props[:contact] << {
26
38
  title: contact.content,
@@ -38,26 +50,22 @@ module MetalArchives
38
50
  when "Address:"
39
51
  props[:address] = content
40
52
  when "Country:"
41
- props[:country] = ParserHelper.parse_country css("a").first.content
53
+ props[:country] = Country.parse(css("a").first.content)
42
54
  when "Phone number:"
43
55
  props[:phone] = content
44
56
  when "Status:"
45
57
  props[:status] = content.downcase.tr(" ", "_").to_sym
46
58
  when "Specialised in:"
47
- props[:specializations] = ParserHelper.parse_genre content
59
+ props[:specializations] = Parsers::Genre.parse(content)
48
60
  when "Founding date :"
49
- begin
50
- dof = Date.parse content
51
- props[:date_founded] = NilDate.new dof.year, dof.month, dof.day
52
- rescue ArgumentError => e
53
- props[:date_founded] = NilDate.parse content
54
- end
61
+ props[:date_founded] = Parsers::Date.parse(content)
55
62
  when "Sub-labels:"
56
63
  # TODO
57
64
  when "Online shopping:"
58
- if content == "Yes"
65
+ case content
66
+ when "Yes"
59
67
  props[:online_shopping] = true
60
- elsif content == "No"
68
+ when "No"
61
69
  props[:online_shopping] = false
62
70
  end
63
71
  else
@@ -1,103 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "date"
4
- require "countries"
5
4
 
6
5
  module MetalArchives
7
- ##
8
- # Mapping layer from and to MA Web Service
9
- #
10
- module Parsers # :nodoc:
6
+ module Parsers
11
7
  ##
12
8
  # Parser base class
13
9
  #
14
10
  class Parser
15
11
  class << self
16
- ##
17
- # Parse a country
18
- #
19
- # Returns +ISO3166::Country+
20
- #
21
- def parse_country(input)
22
- ISO3166::Country.find_country_by_name input
23
- end
24
-
25
12
  ##
26
13
  # Sanitize a string
27
14
  #
28
15
  # Return +String+
29
16
  #
30
17
  def sanitize(input)
31
- input.gsub(/^"/, "").gsub(/"$/, "").strip
18
+ input
19
+ .gsub(/^"/, "")
20
+ .gsub(/"$/, "")
21
+ .gsub(/[[:space:]]/, " ")
22
+ .strip
32
23
  end
33
24
 
34
25
  ##
35
- # Opinionated parsing of genres
36
- #
37
- # Returns an +Array+ of +String+
38
- #
39
- # The following components are omitted:
40
- # - Metal
41
- # - (early)
42
- # - (later)
26
+ # Rewrite a URL
43
27
  #
44
- # All genres are capitalized.
28
+ # Return +URI+
45
29
  #
46
- # For examples on how genres are parsed, refer to +ParserTest#test_parse_genre+
47
- #
48
- def parse_genre(input)
49
- genres = []
50
- # Split fields
51
- input.split(",").each do |genre|
52
- ##
53
- # Start with a single empty genre string. Split the genre by spaces
54
- # and process each component. If a component does not have a slash,
55
- # concatenate it to all genre strings present in +temp+. If it does
56
- # have a slash present, duplicate all genre strings, and concatenate
57
- # the first component (before the slash) to the first half, and the
58
- # last component to the last half. +temp+ now has an array of genre
59
- # combinations.
60
- #
61
- # 'Traditional Heavy/Power Metal' => ['Traditional Heavy', 'Traditional Power']
62
- # 'Traditional/Classical Heavy/Power Metal' => [
63
- # 'Traditional Heavy', 'Traditional Power',
64
- # 'Classical Heavy', 'Classical Power']
65
- #
66
- temp = [""]
67
- genre.downcase.split.reject { |g| ["(early)", "(later)", "metal"].include? g }.each do |g|
68
- if g.include? "/"
69
- # Duplicate all WIP genres
70
- temp2 = temp.dup
71
-
72
- # Assign first and last components to temp and temp2 respectively
73
- split = g.split "/"
74
- temp.map! { |t| t.empty? ? split.first.capitalize : "#{t.capitalize} #{split.first.capitalize}" }
75
- temp2.map! { |t| t.empty? ? split.last.capitalize : "#{t.capitalize} #{split.last.capitalize}" }
76
-
77
- # Add both genre trees
78
- temp += temp2
79
- else
80
- temp.map! { |t| t.empty? ? g.capitalize : "#{t.capitalize} #{g.capitalize}" }
81
- end
82
- end
83
- genres += temp
84
- end
85
- genres.uniq
86
- end
30
+ def rewrite(input)
31
+ return input unless MetalArchives.config.endpoint
87
32
 
88
- ##
89
- # Parse year range
90
- #
91
- def parse_year_range(input)
92
- r = input.split("-")
93
- date_start = (r.first == "?" ? nil : NilDate.new(r.first.to_i))
94
- date_end = if r.length > 1
95
- (r.last == "?" || r.last == "present" ? nil : NilDate.new(r.last.to_i))
96
- else
97
- date_start.dup
98
- end
33
+ endpoint = URI(MetalArchives.config.endpoint)
99
34
 
100
- MetalArchives::Range.new date_start, date_end
35
+ URI(input)
36
+ .tap { |u| u.host = endpoint.host }
37
+ .tap { |u| u.scheme = endpoint.scheme }
38
+ .to_s
101
39
  end
102
40
  end
103
41
  end
@@ -71,7 +71,7 @@ module MetalArchives
71
71
  # +Hash+
72
72
  #
73
73
  def map_params(query)
74
- params = {
74
+ {
75
75
  bandName: query[:band_name] || "",
76
76
  releaseTitle: query[:title] || "",
77
77
  releaseYearFrom: query[:from_year] || "",
@@ -90,8 +90,6 @@ module MetalArchives
90
90
  releaseType: map_types(query[:types]),
91
91
  releaseFormat: map_formats(query[:formats]),
92
92
  }
93
-
94
- params
95
93
  end
96
94
 
97
95
  ##
@@ -103,7 +101,18 @@ module MetalArchives
103
101
  # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
104
102
  #
105
103
  def parse_html(response)
106
- props = {}
104
+ # Set default props
105
+ props = {
106
+ title: nil,
107
+ type: nil,
108
+ date_released: nil,
109
+ catalog_id: nil,
110
+ identifier: nil,
111
+ version_description: nil,
112
+ format: nil,
113
+ limitation: nil,
114
+ }
115
+
107
116
  doc = Nokogiri::HTML response
108
117
 
109
118
  props[:title] = sanitize doc.css("#album_info .album_name a").first.content
@@ -118,12 +127,7 @@ module MetalArchives
118
127
  when "Type:"
119
128
  props[:type] = map_type content
120
129
  when "Release date:"
121
- begin
122
- props[:date_released] = NilDate.parse content
123
- rescue MetalArchives::Errors::ArgumentError => e
124
- dr = Date.parse content
125
- props[:date_released] = NilDate.new dr.year, dr.month, dr.day
126
- end
130
+ props[:date_released] = Parsers::Date.parse(content)
127
131
  when "Catalog ID:"
128
132
  props[:catalog_id] = content
129
133
  when "Identifier:"
@@ -140,7 +144,7 @@ module MetalArchives
140
144
  next if content == "None yet"
141
145
  # TODO: reviews
142
146
  else
143
- raise MetalArchives::Errors::ParserError, "Unknown token: #{dt.content}"
147
+ raise Errors::ParserError, "Unknown token: #{dt.content}"
144
148
  end
145
149
  end
146
150
  end
@@ -178,7 +182,7 @@ module MetalArchives
178
182
 
179
183
  types = []
180
184
  type_syms.each do |type|
181
- raise MetalArchives::Errors::ParserError, "Unknown type: #{type}" unless TYPE_TO_QUERY[type]
185
+ raise Errors::ParserError, "Unknown type: #{type}" unless TYPE_TO_QUERY[type]
182
186
 
183
187
  types << TYPE_TO_QUERY[type]
184
188
  end
@@ -192,7 +196,7 @@ module MetalArchives
192
196
  # Returns +Symbol+, see rdoc-ref:Release.type
193
197
  #
194
198
  def map_type(type)
195
- raise MetalArchives::Errors::ParserError, "Unknown type: #{type}" unless TYPE_TO_SYM[type]
199
+ raise Errors::ParserError, "Unknown type: #{type}" unless TYPE_TO_SYM[type]
196
200
 
197
201
  TYPE_TO_SYM[type]
198
202
  end
@@ -210,7 +214,7 @@ module MetalArchives
210
214
 
211
215
  formats = []
212
216
  format_syms.each do |format|
213
- raise MetalArchives::Errors::ParserError, "Unknown format: #{format}" unless FORMAT_TO_QUERY[format]
217
+ raise Errors::ParserError, "Unknown format: #{format}" unless FORMAT_TO_QUERY[format]
214
218
 
215
219
  formats << FORMAT_TO_QUERY[format]
216
220
  end
@@ -224,11 +228,11 @@ module MetalArchives
224
228
  # Returns +Symbol+, see rdoc-ref:Release.format
225
229
  #
226
230
  def map_format(format)
227
- return :cd if format =~ /CD/
228
- return :vinyl if format =~ /[Vv]inyl/
229
- return :blu_ray if format =~ /[Bb]lu.?[Rr]ay/
231
+ return :cd if /CD/.match?(format)
232
+ return :vinyl if /[Vv]inyl/.match?(format)
233
+ return :blu_ray if /[Bb]lu.?[Rr]ay/.match?(format)
230
234
 
231
- raise MetalArchives::Errors::ParserError, "Unknown format: #{format}" unless FORMAT_TO_SYM[format]
235
+ raise Errors::ParserError, "Unknown format: #{format}" unless FORMAT_TO_SYM[format]
232
236
 
233
237
  FORMAT_TO_SYM[format]
234
238
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ module Parsers
5
+ ##
6
+ # Year range parser
7
+ #
8
+ class Year < Base
9
+ ##
10
+ # Parse year range
11
+ #
12
+ # Returns +Range+ of +Integer+
13
+ #
14
+ def self.parse(input)
15
+ components = input
16
+ .split("-")
17
+ .map(&:to_i)
18
+ .map { |y| y.zero? ? nil : y }
19
+
20
+ return if components.empty?
21
+
22
+ # Set end if only one year
23
+ components << components.first if components.count == 1
24
+
25
+ components[0]..components[1]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -6,8 +6,8 @@ module MetalArchives
6
6
  #
7
7
  module Version
8
8
  MAJOR = 3
9
- MINOR = 0
10
- PATCH = 1
9
+ MINOR = 1
10
+ PATCH = 0
11
11
  PRE = nil
12
12
 
13
13
  VERSION = [MAJOR, MINOR, PATCH].compact.join(".")
@@ -15,5 +15,5 @@ module MetalArchives
15
15
  STRING = [VERSION, PRE].compact.join("-")
16
16
  end
17
17
 
18
- VERSION = MetalArchives::Version::STRING
18
+ VERSION = Version::STRING
19
19
  end
@@ -1,7 +1,10 @@
1
+ ## Environment
2
+ #METAL_ARCHIVES_ENV=development
3
+
1
4
  ## Metal Archives endpoint
2
- # MA_ENDPOINT=https://www.metal-archives.com/
3
- # MA_ENDPOINT_USER=my_user
4
- # MA_ENDPOINT_PASSWORD=my_password
5
+ #MA_ENDPOINT=https://www.metal-archives.com/
6
+ #MA_ENDPOINT_USER=my_user
7
+ #MA_ENDPOINT_PASSWORD=my_password
5
8
 
6
9
  ## WebMock
7
- # WEBMOCK_ALLOW_HOST=metal-archives.com
10
+ #WEBMOCK_ALLOW_HOST=metal-archives.com