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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +59 -12
- data/.rspec +1 -0
- data/.rubocop.yml +34 -20
- data/CHANGELOG.md +4 -0
- data/LICENSE.md +17 -4
- data/README.md +29 -14
- data/bin/console +8 -11
- data/config/inflections.rb +7 -0
- data/config/initializers/.keep +0 -0
- data/docker-compose.yml +10 -1
- data/lib/metal_archives.rb +56 -22
- data/lib/metal_archives/cache/base.rb +40 -0
- data/lib/metal_archives/cache/memory.rb +68 -0
- data/lib/metal_archives/cache/null.rb +22 -0
- data/lib/metal_archives/cache/redis.rb +49 -0
- data/lib/metal_archives/collection.rb +3 -5
- data/lib/metal_archives/configuration.rb +28 -21
- data/lib/metal_archives/errors.rb +9 -1
- data/lib/metal_archives/http_client.rb +42 -46
- data/lib/metal_archives/models/artist.rb +55 -26
- data/lib/metal_archives/models/band.rb +43 -36
- data/lib/metal_archives/models/{base_model.rb → base.rb} +53 -53
- data/lib/metal_archives/models/label.rb +7 -8
- data/lib/metal_archives/models/release.rb +11 -17
- data/lib/metal_archives/parsers/artist.rb +40 -35
- data/lib/metal_archives/parsers/band.rb +73 -29
- data/lib/metal_archives/parsers/base.rb +14 -0
- data/lib/metal_archives/parsers/country.rb +21 -0
- data/lib/metal_archives/parsers/date.rb +31 -0
- data/lib/metal_archives/parsers/genre.rb +67 -0
- data/lib/metal_archives/parsers/label.rb +21 -13
- data/lib/metal_archives/parsers/parser.rb +15 -77
- data/lib/metal_archives/parsers/release.rb +22 -18
- data/lib/metal_archives/parsers/year.rb +29 -0
- data/lib/metal_archives/version.rb +3 -3
- data/metal_archives.env.example +7 -4
- data/metal_archives.gemspec +7 -4
- data/nginx/default.conf +2 -2
- metadata +76 -32
- data/.github/workflows/release.yml +0 -69
- data/.rubocop_todo.yml +0 -92
- data/lib/metal_archives/lru_cache.rb +0 -61
- data/lib/metal_archives/middleware/cache_check.rb +0 -18
- data/lib/metal_archives/middleware/encoding.rb +0 -16
- data/lib/metal_archives/middleware/headers.rb +0 -38
- data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
- data/lib/metal_archives/nil_date.rb +0 -91
- 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.
|
14
|
+
"#{MetalArchives.config.endpoint}labels/#{params[:name]}/#{params[:id]}"
|
15
15
|
end
|
16
16
|
|
17
17
|
def parse(response)
|
18
|
-
|
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] =
|
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] =
|
59
|
+
props[:specializations] = Parsers::Genre.parse(content)
|
48
60
|
when "Founding date :"
|
49
|
-
|
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
|
-
|
65
|
+
case content
|
66
|
+
when "Yes"
|
59
67
|
props[:online_shopping] = true
|
60
|
-
|
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
|
18
|
+
input
|
19
|
+
.gsub(/^"/, "")
|
20
|
+
.gsub(/"$/, "")
|
21
|
+
.gsub(/[[:space:]]/, " ")
|
22
|
+
.strip
|
32
23
|
end
|
33
24
|
|
34
25
|
##
|
35
|
-
#
|
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
|
-
#
|
28
|
+
# Return +URI+
|
45
29
|
#
|
46
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
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
|
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
|
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
|
228
|
-
return :vinyl if
|
229
|
-
return :blu_ray if
|
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
|
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 =
|
10
|
-
PATCH =
|
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 =
|
18
|
+
VERSION = Version::STRING
|
19
19
|
end
|
data/metal_archives.env.example
CHANGED
@@ -1,7 +1,10 @@
|
|
1
|
+
## Environment
|
2
|
+
#METAL_ARCHIVES_ENV=development
|
3
|
+
|
1
4
|
## Metal Archives endpoint
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
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
|
-
#
|
10
|
+
#WEBMOCK_ALLOW_HOST=metal-archives.com
|