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.
- 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
|