metal_archives 3.0.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
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