metal_archives 0.8.0 → 1.0.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/.gitignore +59 -7
- data/.rspec +1 -0
- data/.rubocop.yml +14 -0
- data/.travis.yml +11 -0
- data/Gemfile +2 -0
- data/{LICENSE → LICENSE.md} +0 -0
- data/README.md +77 -9
- data/Rakefile +5 -3
- data/lib/metal_archives.rb +8 -0
- data/lib/metal_archives/configuration.rb +28 -7
- data/lib/metal_archives/error.rb +37 -30
- data/lib/metal_archives/http_client.rb +21 -42
- data/lib/metal_archives/middleware/headers.rb +38 -0
- data/lib/metal_archives/middleware/rewrite_endpoint.rb +38 -0
- data/lib/metal_archives/models/artist.rb +51 -65
- data/lib/metal_archives/models/band.rb +41 -39
- data/lib/metal_archives/models/base_model.rb +88 -59
- data/lib/metal_archives/models/label.rb +7 -6
- data/lib/metal_archives/parsers/artist.rb +110 -99
- data/lib/metal_archives/parsers/band.rb +168 -156
- data/lib/metal_archives/parsers/label.rb +54 -52
- data/lib/metal_archives/parsers/parser.rb +73 -71
- data/lib/metal_archives/utils/collection.rb +7 -1
- data/lib/metal_archives/utils/lru_cache.rb +11 -4
- data/lib/metal_archives/utils/nil_date.rb +54 -0
- data/lib/metal_archives/utils/range.rb +16 -8
- data/lib/metal_archives/version.rb +3 -1
- data/metal_archives.gemspec +21 -11
- data/spec/configuration_spec.rb +101 -0
- data/spec/factories/artist_factory.rb +37 -0
- data/spec/factories/band_factory.rb +60 -0
- data/spec/factories/nil_date_factory.rb +9 -0
- data/spec/factories/range_factory.rb +8 -0
- data/spec/models/artist_spec.rb +142 -0
- data/spec/models/band_spec.rb +179 -0
- data/spec/models/base_model_spec.rb +217 -0
- data/spec/parser_spec.rb +19 -0
- data/spec/spec_helper.rb +111 -0
- data/spec/support/factory_girl.rb +5 -0
- data/spec/support/metal_archives.rb +26 -0
- data/spec/utils/collection_spec.rb +72 -0
- data/spec/utils/lru_cache_spec.rb +53 -0
- data/spec/utils/nil_date_spec.rb +98 -0
- data/spec/utils/range_spec.rb +62 -0
- metadata +142 -57
- data/test/base_model_test.rb +0 -111
- data/test/configuration_test.rb +0 -57
- data/test/parser_test.rb +0 -37
- data/test/property/artist_property_test.rb +0 -43
- data/test/property/band_property_test.rb +0 -94
- data/test/query/artist_query_test.rb +0 -109
- data/test/query/band_query_test.rb +0 -152
- data/test/test_helper.rb +0 -25
- data/test/utils/collection_test.rb +0 -51
- data/test/utils/lru_cache_test.rb +0 -22
- data/test/utils/range_test.rb +0 -42
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module MetalArchives
|
2
3
|
##
|
3
4
|
# Abstract model class
|
@@ -7,7 +8,7 @@ module MetalArchives
|
|
7
8
|
# Generic shallow copy constructor
|
8
9
|
#
|
9
10
|
def initialize(hash = {})
|
10
|
-
raise Errors::NotImplementedError, 'no :id property in model' unless
|
11
|
+
raise Errors::NotImplementedError, 'no :id property in model' unless respond_to? :id?, true
|
11
12
|
|
12
13
|
hash.each do |property, value|
|
13
14
|
instance_variable_set("@#{property}", value) if self.class.properties.include? property
|
@@ -18,7 +19,7 @@ module MetalArchives
|
|
18
19
|
# Returns true if two objects have the same type and id
|
19
20
|
#
|
20
21
|
def ==(obj)
|
21
|
-
obj.instance_of?
|
22
|
+
obj.instance_of?(self.class) && id ==(obj.id)
|
22
23
|
end
|
23
24
|
|
24
25
|
##
|
@@ -33,9 +34,31 @@ module MetalArchives
|
|
33
34
|
|
34
35
|
# Use constructor to set attributes
|
35
36
|
initialize assemble
|
37
|
+
|
38
|
+
@loaded = true
|
39
|
+
self.class.cache[id] = self
|
40
|
+
rescue => e
|
41
|
+
# Don't cache invalid requests
|
42
|
+
self.class.cache.delete id
|
43
|
+
raise e
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Whether or not the object is currently loaded
|
48
|
+
#
|
49
|
+
def loaded?
|
50
|
+
!@loaded.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
##
|
54
|
+
# Whether or not the object is currently cached
|
55
|
+
#
|
56
|
+
def cached?
|
57
|
+
loaded? && self.class.cache.include?(id)
|
36
58
|
end
|
37
59
|
|
38
60
|
protected
|
61
|
+
|
39
62
|
##
|
40
63
|
# Fetch the data and assemble the model
|
41
64
|
#
|
@@ -45,9 +68,9 @@ module MetalArchives
|
|
45
68
|
# - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
|
46
69
|
# - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
|
47
70
|
#
|
48
|
-
|
49
|
-
|
50
|
-
|
71
|
+
def assemble
|
72
|
+
raise Errors::NotImplementedError, 'method :assemble not implemented'
|
73
|
+
end
|
51
74
|
|
52
75
|
class << self
|
53
76
|
##
|
@@ -63,6 +86,7 @@ module MetalArchives
|
|
63
86
|
end
|
64
87
|
|
65
88
|
protected
|
89
|
+
|
66
90
|
##
|
67
91
|
# Defines a model property.
|
68
92
|
#
|
@@ -79,39 +103,44 @@ module MetalArchives
|
|
79
103
|
# Whether or not the property has multiple values (which
|
80
104
|
# turns it into an +Array+ of +type+)
|
81
105
|
#
|
82
|
-
|
83
|
-
|
106
|
+
def property(name, opts = {})
|
107
|
+
(@properties ||= []) << name
|
84
108
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
109
|
+
# property
|
110
|
+
define_method(name) do
|
111
|
+
load! unless instance_variable_defined?("@#{name}") || name ==(:id)
|
112
|
+
instance_variable_get("@#{name}")
|
113
|
+
end
|
90
114
|
|
91
|
-
|
92
|
-
|
93
|
-
|
115
|
+
# property?
|
116
|
+
define_method("#{name}?") do
|
117
|
+
load! unless instance_variable_defined?("@#{name}") || name ==(:id)
|
94
118
|
|
95
|
-
|
96
|
-
|
119
|
+
property = instance_variable_get("@#{name}")
|
120
|
+
property.respond_to?(:empty?) ? !property.empty? : !!property
|
121
|
+
end
|
122
|
+
|
123
|
+
# property=
|
124
|
+
define_method("#{name}=") do |value|
|
125
|
+
if value.nil?
|
126
|
+
instance_variable_set "@#{name}", value
|
127
|
+
return
|
97
128
|
end
|
98
129
|
|
99
|
-
#
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
raise MetalArchives::Errors::TypeError, "invalid type #{
|
105
|
-
value.each do |val|
|
106
|
-
raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
|
107
|
-
end
|
108
|
-
else
|
109
|
-
raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
|
130
|
+
# Check value type
|
131
|
+
type = opts[:type] || String
|
132
|
+
if opts[:multiple]
|
133
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
|
134
|
+
value.each do |val|
|
135
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
|
110
136
|
end
|
111
|
-
|
112
|
-
|
137
|
+
else
|
138
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
|
113
139
|
end
|
140
|
+
|
141
|
+
instance_variable_set "@#{name}", value
|
114
142
|
end
|
143
|
+
end
|
115
144
|
|
116
145
|
##
|
117
146
|
# Defines a model enum property.
|
@@ -127,40 +156,40 @@ module MetalArchives
|
|
127
156
|
# Whether or not the property has multiple values (which
|
128
157
|
# turns it into an +Array+ of +type+)
|
129
158
|
#
|
130
|
-
|
131
|
-
|
159
|
+
def enum(name, opts)
|
160
|
+
raise ArgumentError, 'opts[:values] is required' unless opts and opts[:values]
|
132
161
|
|
133
|
-
|
162
|
+
(@properties ||= []) << name
|
134
163
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
164
|
+
# property
|
165
|
+
define_method(name) do
|
166
|
+
load! unless instance_variable_defined?("@#{name}")
|
167
|
+
instance_variable_get("@#{name}")
|
168
|
+
end
|
140
169
|
|
141
|
-
|
142
|
-
|
143
|
-
|
170
|
+
# property?
|
171
|
+
define_method("#{name}?") do
|
172
|
+
load! unless instance_variable_defined?("@#{name}")
|
144
173
|
|
145
|
-
|
146
|
-
|
147
|
-
|
174
|
+
property = instance_variable_get("@#{name}")
|
175
|
+
property.respond_to?(:empty?) ? !property.empty? : !!property
|
176
|
+
end
|
148
177
|
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
157
|
-
else
|
158
|
-
raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
|
178
|
+
# property=
|
179
|
+
define_method("#{name}=") do |value|
|
180
|
+
# Check enum type
|
181
|
+
if opts[:multiple]
|
182
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
|
183
|
+
value.each do |val|
|
184
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
|
159
185
|
end
|
160
|
-
|
161
|
-
|
186
|
+
else
|
187
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
|
162
188
|
end
|
189
|
+
|
190
|
+
instance_variable_set name, value
|
163
191
|
end
|
192
|
+
end
|
164
193
|
|
165
194
|
##
|
166
195
|
# Defines a model boolean property. This method is an alias for <tt>enum name, :values => [true, false]</tt>
|
@@ -173,9 +202,9 @@ module MetalArchives
|
|
173
202
|
# Whether or not the property has multiple values (which
|
174
203
|
# turns it into an +Array+ of +type+)
|
175
204
|
#
|
176
|
-
|
177
|
-
|
178
|
-
|
205
|
+
def boolean(name, opts = {})
|
206
|
+
enum name, opts.merge(:values => [true, false])
|
207
|
+
end
|
179
208
|
end
|
180
209
|
end
|
181
210
|
end
|
@@ -1,8 +1,9 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'date'
|
2
4
|
require 'countries'
|
3
5
|
|
4
6
|
module MetalArchives
|
5
|
-
|
6
7
|
##
|
7
8
|
# Represents a record label
|
8
9
|
#
|
@@ -82,7 +83,7 @@ module MetalArchives
|
|
82
83
|
#
|
83
84
|
# Returns +:active+, +:closed+ or +:unknown+
|
84
85
|
#
|
85
|
-
enum :status, :values => [
|
86
|
+
enum :status, :values => %i[active closed unknown]
|
86
87
|
|
87
88
|
class << self
|
88
89
|
##
|
@@ -90,7 +91,7 @@ module MetalArchives
|
|
90
91
|
#
|
91
92
|
# Returns +Array+ of rdoc-ref:Label
|
92
93
|
#
|
93
|
-
def search(
|
94
|
+
def search(_name)
|
94
95
|
results = []
|
95
96
|
results
|
96
97
|
end
|
@@ -102,9 +103,9 @@ module MetalArchives
|
|
102
103
|
#
|
103
104
|
def find_by_name(name, id)
|
104
105
|
client.find_resource(
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
:band,
|
107
|
+
:name => name,
|
108
|
+
:id => id
|
108
109
|
)
|
109
110
|
end
|
110
111
|
end
|
@@ -1,126 +1,137 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'json'
|
2
4
|
require 'date'
|
3
5
|
require 'countries'
|
4
6
|
|
7
|
+
require 'metal_archives/middleware/rewrite_endpoint'
|
8
|
+
|
5
9
|
module MetalArchives
|
6
|
-
module Parsers
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
10
|
+
module Parsers
|
11
|
+
##
|
12
|
+
# Artist parser
|
13
|
+
#
|
14
|
+
class Artist < Parser # :nodoc:
|
15
|
+
class << self
|
16
|
+
##
|
17
|
+
# Map attributes to MA attributes
|
18
|
+
#
|
19
|
+
# Returns +Hash+
|
20
|
+
#
|
21
|
+
# [+params+]
|
22
|
+
# +Hash+
|
23
|
+
#
|
24
|
+
def map_params(query)
|
25
|
+
params = {
|
26
|
+
:query => query[:name] || ''
|
27
|
+
}
|
28
|
+
|
29
|
+
params
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Parse main HTML page
|
34
|
+
#
|
35
|
+
# Returns +Hash+
|
36
|
+
#
|
37
|
+
# [Raises]
|
38
|
+
# - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
|
39
|
+
#
|
40
|
+
def parse_html(response)
|
41
|
+
props = {}
|
42
|
+
doc = Nokogiri::HTML response
|
43
|
+
|
44
|
+
# Photo
|
45
|
+
unless doc.css('.member_img').empty?
|
46
|
+
photo_uri = URI doc.css('.member_img img').first.attr('src')
|
47
|
+
props[:photo] = Middleware::RewriteEndpoint.rewrite photo_uri
|
48
|
+
end
|
49
|
+
|
50
|
+
doc.css('#member_info dl').each do |dl|
|
51
|
+
dl.css('dt').each do |dt|
|
52
|
+
content = sanitize(dt.next_element.content)
|
53
|
+
|
54
|
+
next if content == 'N/A'
|
27
55
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
props[:date_of_birth] = Date.parse date
|
52
|
-
when 'R.I.P.:'
|
53
|
-
props[:date_of_death] = Date.parse content
|
54
|
-
when 'Died of:'
|
55
|
-
props[:cause_of_death] = content
|
56
|
-
when 'Place of origin:'
|
57
|
-
props[:country] = ISO3166::Country.find_country_by_name(sanitize(dt.next_element.css('a').first.content))
|
58
|
-
location = dt.next_element.xpath('text()').map { |x| x.content }.join('').strip.gsub(/[()]/, '')
|
59
|
-
props[:location] = location unless location.empty?
|
60
|
-
when 'Gender:'
|
61
|
-
case content
|
62
|
-
when 'Male'
|
63
|
-
props[:gender] = :male
|
64
|
-
when 'Female'
|
65
|
-
props[:gender] = :female
|
56
|
+
case sanitize(dt.content)
|
57
|
+
when 'Real/full name:'
|
58
|
+
props[:name] = content
|
59
|
+
when 'Age:'
|
60
|
+
date = content.strip.gsub(/[0-9]* *\(born ([^\)]*)\)/, '\1')
|
61
|
+
props[:date_of_birth] = Date.parse date
|
62
|
+
when 'R.I.P.:'
|
63
|
+
props[:date_of_death] = Date.parse content
|
64
|
+
when 'Died of:'
|
65
|
+
props[:cause_of_death] = content
|
66
|
+
when 'Place of origin:'
|
67
|
+
props[:country] = ISO3166::Country.find_country_by_name(sanitize(dt.next_element.css('a').first.content))
|
68
|
+
location = dt.next_element.xpath('text()').map(&:content).join('').strip.gsub(/[()]/, '')
|
69
|
+
props[:location] = location unless location.empty?
|
70
|
+
when 'Gender:'
|
71
|
+
case content
|
72
|
+
when 'Male'
|
73
|
+
props[:gender] = :male
|
74
|
+
when 'Female'
|
75
|
+
props[:gender] = :female
|
76
|
+
else
|
77
|
+
raise Errors::ParserError, "Unknown gender: #{content}"
|
78
|
+
end
|
66
79
|
else
|
67
|
-
raise Errors::ParserError, "Unknown
|
80
|
+
raise Errors::ParserError, "Unknown token: #{dt.content}"
|
68
81
|
end
|
69
|
-
else
|
70
|
-
raise Errors::ParserError, "Unknown token: #{dt.content}"
|
71
82
|
end
|
72
83
|
end
|
73
|
-
end
|
74
84
|
|
75
|
-
|
76
|
-
|
77
|
-
|
85
|
+
# Aliases
|
86
|
+
props[:aliases] = []
|
87
|
+
alt = sanitize doc.css('.band_member_name').first.content
|
88
|
+
props[:aliases] << alt unless props[:name] == alt
|
78
89
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
90
|
+
props
|
91
|
+
rescue => e
|
92
|
+
e.backtrace.each { |b| MetalArchives.config.logger.error b }
|
93
|
+
raise Errors::ParserError, e
|
94
|
+
end
|
84
95
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
96
|
+
##
|
97
|
+
# Parse links HTML page
|
98
|
+
#
|
99
|
+
# Returns +Hash+
|
100
|
+
#
|
101
|
+
# [Raises]
|
102
|
+
# - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
|
103
|
+
#
|
104
|
+
def parse_links_html(response)
|
105
|
+
links = []
|
95
106
|
|
96
|
-
|
107
|
+
doc = Nokogiri::HTML response
|
97
108
|
|
98
|
-
|
99
|
-
|
109
|
+
# Default to official links
|
110
|
+
type = :official
|
100
111
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
112
|
+
doc.css('#linksTablemain tr').each do |row|
|
113
|
+
if row['id'].match?(/^header_/)
|
114
|
+
type = row['id'].gsub(/^header_/, '').downcase.to_sym
|
115
|
+
else
|
116
|
+
a = row.css('td a').first
|
106
117
|
|
107
|
-
|
108
|
-
|
118
|
+
# No links have been added yet
|
119
|
+
next unless a
|
109
120
|
|
110
|
-
|
121
|
+
links << {
|
111
122
|
:url => a['href'],
|
112
123
|
:type => type,
|
113
124
|
:title => a.content
|
114
|
-
|
125
|
+
}
|
126
|
+
end
|
115
127
|
end
|
116
|
-
end
|
117
128
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
129
|
+
links
|
130
|
+
rescue => e
|
131
|
+
e.backtrace.each { |b| MetalArchives.config.logger.error b }
|
132
|
+
raise Errors::ParserError, e
|
133
|
+
end
|
122
134
|
end
|
123
135
|
end
|
124
136
|
end
|
125
137
|
end
|
126
|
-
end
|