metal_archives 0.2.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 +7 -0
- data/.gitignore +11 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/LICENSE +0 -0
- data/README.md +88 -0
- data/Rakefile +14 -0
- data/lib/metal_archives.rb +22 -0
- data/lib/metal_archives/configuration.rb +81 -0
- data/lib/metal_archives/error.rb +35 -0
- data/lib/metal_archives/http_client.rb +75 -0
- data/lib/metal_archives/models/artist.rb +216 -0
- data/lib/metal_archives/models/band.rb +291 -0
- data/lib/metal_archives/models/base_model.rb +153 -0
- data/lib/metal_archives/models/label.rb +112 -0
- data/lib/metal_archives/models/range.rb +58 -0
- data/lib/metal_archives/parsers/artist.rb +103 -0
- data/lib/metal_archives/parsers/band.rb +160 -0
- data/lib/metal_archives/parsers/label.rb +68 -0
- data/lib/metal_archives/parsers/parser_helper.rb +79 -0
- data/lib/metal_archives/version.rb +6 -0
- data/metal_archives.gemspec +28 -0
- data/test/base_model_test.rb +87 -0
- data/test/configuration_test.rb +57 -0
- data/test/parser_helper_test.rb +37 -0
- data/test/property/artist_property_test.rb +43 -0
- data/test/property/band_property_test.rb +94 -0
- data/test/query/artist_query_test.rb +44 -0
- data/test/query/band_query_test.rb +84 -0
- data/test/range_test.rb +41 -0
- data/test/test_helper.rb +26 -0
- metadata +214 -0
@@ -0,0 +1,291 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'countries'
|
3
|
+
|
4
|
+
module MetalArchives
|
5
|
+
|
6
|
+
##
|
7
|
+
# Represents an band (person or group)
|
8
|
+
#
|
9
|
+
class Band < BaseModel
|
10
|
+
##
|
11
|
+
# :attr_reader: id
|
12
|
+
#
|
13
|
+
# Returns +Integer+
|
14
|
+
#
|
15
|
+
property :id, :type => Integer
|
16
|
+
|
17
|
+
##
|
18
|
+
# :attr_reader: name
|
19
|
+
#
|
20
|
+
# Returns +String+
|
21
|
+
#
|
22
|
+
property :name
|
23
|
+
|
24
|
+
##
|
25
|
+
# :attr_reader: aliases
|
26
|
+
#
|
27
|
+
# Returns +Array+ of +String+
|
28
|
+
#
|
29
|
+
property :aliases, :multiple => true
|
30
|
+
|
31
|
+
##
|
32
|
+
# :attr_reader: country
|
33
|
+
#
|
34
|
+
# Returns +ISO3166::Country+
|
35
|
+
#
|
36
|
+
property :country, :type => ISO3166::Country
|
37
|
+
|
38
|
+
##
|
39
|
+
# :attr_reader: location
|
40
|
+
#
|
41
|
+
# Returns +String+
|
42
|
+
#
|
43
|
+
property :location
|
44
|
+
|
45
|
+
##
|
46
|
+
# :attr_reader: date_formed
|
47
|
+
#
|
48
|
+
# Returns +Date+
|
49
|
+
#
|
50
|
+
property :date_formed, :type => Date
|
51
|
+
|
52
|
+
##
|
53
|
+
# :attr_reader: date_active
|
54
|
+
#
|
55
|
+
# Returns +Array+ of rdoc-ref:Range
|
56
|
+
#
|
57
|
+
property :date_active, :type => MetalArchives::Range, :multiple => true
|
58
|
+
|
59
|
+
##
|
60
|
+
# :attr_reader: genres
|
61
|
+
#
|
62
|
+
# Returns +Array+ of +String+
|
63
|
+
#
|
64
|
+
property :genres, :multiple => true
|
65
|
+
|
66
|
+
##
|
67
|
+
# :attr_reader: lyrical_themes
|
68
|
+
#
|
69
|
+
# Returns +Array+ of +String+
|
70
|
+
#
|
71
|
+
property :lyrical_themes, :multiple => true
|
72
|
+
|
73
|
+
##
|
74
|
+
# :attr_reader: label
|
75
|
+
#
|
76
|
+
# Returns rdoc-ref:Label
|
77
|
+
#
|
78
|
+
property :label, :type => MetalArchives::Label
|
79
|
+
|
80
|
+
##
|
81
|
+
# :attr_reader: independent
|
82
|
+
#
|
83
|
+
# Returns boolean
|
84
|
+
#
|
85
|
+
enum :independent, :values => [true, false]
|
86
|
+
|
87
|
+
##
|
88
|
+
# :attr_reader: comment
|
89
|
+
#
|
90
|
+
# Returns raw HTML +String+
|
91
|
+
#
|
92
|
+
property :comment
|
93
|
+
|
94
|
+
##
|
95
|
+
# :attr_reader: status
|
96
|
+
#
|
97
|
+
# Returns +:active+, +:split_up+, +:on_hold+, +:unknown+, +:changed_name+ or +:disputed+
|
98
|
+
#
|
99
|
+
enum :status, :values => [:active, :split_up, :on_hold, :unknown, :changed_name, :disputed]
|
100
|
+
|
101
|
+
# TODO: releases
|
102
|
+
# TODO: members
|
103
|
+
|
104
|
+
##
|
105
|
+
# :attr_reader: similar
|
106
|
+
#
|
107
|
+
# Returns +Array+ of +Hash+ containing the following keys
|
108
|
+
#
|
109
|
+
# [+similar+]
|
110
|
+
# - +:band+: rdoc-ref:Band
|
111
|
+
# - +:score+: +Integer+
|
112
|
+
#
|
113
|
+
property :similar, :type => Hash, :multiple => true
|
114
|
+
|
115
|
+
##
|
116
|
+
# :attr_reader: logo
|
117
|
+
#
|
118
|
+
# Returns +String+
|
119
|
+
#
|
120
|
+
property :logo
|
121
|
+
|
122
|
+
##
|
123
|
+
# :attr_reader: photo
|
124
|
+
#
|
125
|
+
# Returns +String+
|
126
|
+
#
|
127
|
+
property :photo
|
128
|
+
|
129
|
+
##
|
130
|
+
# :attr_reader: links
|
131
|
+
#
|
132
|
+
# Returns +Array+ of +Hash+ containing the following keys
|
133
|
+
#
|
134
|
+
# [+similar+]
|
135
|
+
# - +:url+: +String+
|
136
|
+
# - +:type+: +Symbol+, either +:official+ or +:merchandise+
|
137
|
+
# - +:title+: +String+
|
138
|
+
#
|
139
|
+
property :links, :multiple => true
|
140
|
+
|
141
|
+
protected
|
142
|
+
##
|
143
|
+
# Fetch the data and assemble the model
|
144
|
+
#
|
145
|
+
# Raises rdoc-ref:MetalArchives::Errors::APIError
|
146
|
+
#
|
147
|
+
def assemble # :nodoc:
|
148
|
+
## Base attributes
|
149
|
+
url = "http://www.metal-archives.com/band/view/id/#{id}"
|
150
|
+
response = HTTPClient.get url
|
151
|
+
|
152
|
+
properties = Parsers::Band.parse_html response.body
|
153
|
+
|
154
|
+
## Comment
|
155
|
+
url = "http://www.metal-archives.com/band/read-more/id/#{id}"
|
156
|
+
response = HTTPClient.get url
|
157
|
+
|
158
|
+
properties[:comment] = response.body
|
159
|
+
|
160
|
+
## Similar artists
|
161
|
+
url = "http://www.metal-archives.com/band/ajax-recommendations/id/#{id}"
|
162
|
+
response = HTTPClient.get url
|
163
|
+
|
164
|
+
properties[:similar] = Parsers::Band.parse_similar_bands_html response.body
|
165
|
+
|
166
|
+
## Related links
|
167
|
+
url = "http://www.metal-archives.com/link/ajax-list/type/band/id/#{id}"
|
168
|
+
response = HTTPClient.get url
|
169
|
+
|
170
|
+
properties[:links] = Parsers::Band.parse_related_links_html response.body
|
171
|
+
|
172
|
+
## Use constructor to fill properties
|
173
|
+
initialize properties
|
174
|
+
end
|
175
|
+
|
176
|
+
class << self
|
177
|
+
##
|
178
|
+
# Find by ID
|
179
|
+
#
|
180
|
+
# Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
|
181
|
+
#
|
182
|
+
# Returns rdoc-ref:Band, even when ID is invalid (because the data is lazily fetched)
|
183
|
+
#
|
184
|
+
# [+id+]
|
185
|
+
# +Integer+
|
186
|
+
#
|
187
|
+
def find(id)
|
188
|
+
Band.new :id => id
|
189
|
+
end
|
190
|
+
|
191
|
+
##
|
192
|
+
# Find by attributes
|
193
|
+
#
|
194
|
+
# Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
|
195
|
+
#
|
196
|
+
# Returns rdoc-ref:Band or nil when ID is invalid
|
197
|
+
#
|
198
|
+
# [+query+]
|
199
|
+
# Hash containing one or more of the following keys:
|
200
|
+
# - +:name+: +String+
|
201
|
+
# - +:exact+: +Boolean+
|
202
|
+
# - +:genre+: +String+
|
203
|
+
# - +:country+: +ISO366::Country+
|
204
|
+
# - +:year_formation+: rdoc-ref:Range of +Date+
|
205
|
+
# - +:comment+: +String+
|
206
|
+
# - +:status+: see rdoc-ref:Band.status
|
207
|
+
# - +:lyrical_themes+: +String+
|
208
|
+
# - +:location+: +String+
|
209
|
+
# - +:label+: rdoc-ref:Label
|
210
|
+
# - +:independent+: boolean
|
211
|
+
#
|
212
|
+
def find_by(query)
|
213
|
+
url = 'http://www.metal-archives.com/search/ajax-advanced/searching/bands/'
|
214
|
+
params = Parsers::Band.map_params query
|
215
|
+
|
216
|
+
response = HTTPClient.get url, params
|
217
|
+
json = JSON.parse response.body
|
218
|
+
|
219
|
+
return nil if json['aaData'].empty?
|
220
|
+
|
221
|
+
data = json['aaData'].first
|
222
|
+
id = Nokogiri::HTML(data.first).xpath('//a/@href').first.value.gsub('\\', '').split('/').last.gsub(/\D/, '').to_i
|
223
|
+
|
224
|
+
Band.new :id => id
|
225
|
+
rescue Errors::APIError
|
226
|
+
nil
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Search by attributes
|
231
|
+
#
|
232
|
+
# Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
|
233
|
+
#
|
234
|
+
# Returns (possibly empty) +Array+ of rdoc-ref:Band
|
235
|
+
#
|
236
|
+
# [+query+]
|
237
|
+
# Hash containing one or more of the following keys:
|
238
|
+
# - +:name+: +String+
|
239
|
+
# - +:exact+: +Boolean+
|
240
|
+
# - +:genre+: +String+
|
241
|
+
# - +:country+: +ISO366::Country+
|
242
|
+
# - +:year_formation+: rdoc-ref:Range of +Date+
|
243
|
+
# - +:comment+: +String+
|
244
|
+
# - +:status+: see rdoc-ref:Band.status
|
245
|
+
# - +:lyrical_themes+: +String+
|
246
|
+
# - +:location+: +String+
|
247
|
+
# - +:label+: rdoc-ref:Label
|
248
|
+
# - +:independent+: boolean
|
249
|
+
#
|
250
|
+
def search_by(query)
|
251
|
+
objects = []
|
252
|
+
|
253
|
+
url = 'http://www.metal-archives.com/search/ajax-advanced/searching/bands/'
|
254
|
+
query[:iDisplayStart] = 0
|
255
|
+
|
256
|
+
loop do
|
257
|
+
params = Parsers::Band.map_params query
|
258
|
+
|
259
|
+
response = HTTPClient.get url, params
|
260
|
+
json = JSON.parse response.body
|
261
|
+
|
262
|
+
json['aaData'].each do |data|
|
263
|
+
# Create Band object for every ID in the results list
|
264
|
+
id = Nokogiri::HTML(data.first).xpath('//a/@href').first.value.gsub('\\', '').split('/').last.gsub(/\D/, '').to_i
|
265
|
+
objects << Band.new(:id => id)
|
266
|
+
end
|
267
|
+
|
268
|
+
break if objects.length == json['iTotalRecords']
|
269
|
+
|
270
|
+
query[:iDisplayStart] += 200
|
271
|
+
end
|
272
|
+
|
273
|
+
objects
|
274
|
+
end
|
275
|
+
|
276
|
+
##
|
277
|
+
# Search by name, resolves to rdoc-ref:Band.search_by <tt>(:name => name)</tt>
|
278
|
+
#
|
279
|
+
# Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
|
280
|
+
#
|
281
|
+
# Returns (possibly empty) +Array+ of rdoc-ref:Band
|
282
|
+
#
|
283
|
+
# [+name+]
|
284
|
+
# +String+
|
285
|
+
#
|
286
|
+
def search(name)
|
287
|
+
search_by :name => name
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module MetalArchives
|
2
|
+
##
|
3
|
+
# Base model class all models are derived from
|
4
|
+
#
|
5
|
+
class BaseModel # :nodoc:
|
6
|
+
##
|
7
|
+
# Generic shallow copy constructor
|
8
|
+
#
|
9
|
+
def initialize(hash = {})
|
10
|
+
raise Errors::NotImplementedError, 'no :id property in model' unless self.respond_to? :id?, true
|
11
|
+
|
12
|
+
hash.each do |property, value|
|
13
|
+
instance_variable_set("@#{property}", value) if self.class.properties.include? property
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
##
|
19
|
+
# Eagerly fetch the data
|
20
|
+
#
|
21
|
+
# Raises rdoc-ref:MetalArchives::Errors::APIError
|
22
|
+
#
|
23
|
+
def fetch
|
24
|
+
raise Errors::DataError, 'no id present' unless !!id
|
25
|
+
|
26
|
+
raise Errors::NotImplementedError, 'no :assemble method in model' unless self.respond_to? :assemble, true
|
27
|
+
|
28
|
+
assemble
|
29
|
+
end
|
30
|
+
|
31
|
+
class << self
|
32
|
+
##
|
33
|
+
# +Array+ of declared properties
|
34
|
+
#
|
35
|
+
attr_accessor :properties
|
36
|
+
|
37
|
+
protected
|
38
|
+
##
|
39
|
+
# Defines a model property.
|
40
|
+
#
|
41
|
+
# [+name+]
|
42
|
+
# Name of the property
|
43
|
+
#
|
44
|
+
# [+opts+]
|
45
|
+
# [+type+]
|
46
|
+
# Data type of property (a constant)
|
47
|
+
#
|
48
|
+
# Default: +String+
|
49
|
+
#
|
50
|
+
# [+multiple+]
|
51
|
+
# Whether or not the property has multiple values (which
|
52
|
+
# turns it into an +Array+ of +type+)
|
53
|
+
#
|
54
|
+
def property(name, opts = {})
|
55
|
+
(@properties ||= []) << name
|
56
|
+
|
57
|
+
# property
|
58
|
+
define_method(name) do
|
59
|
+
self.fetch unless instance_variable_defined?("@#{name}") or name == :id
|
60
|
+
instance_variable_get("@#{name}")
|
61
|
+
end
|
62
|
+
|
63
|
+
# property?
|
64
|
+
define_method("#{name}?") do
|
65
|
+
self.fetch unless instance_variable_defined?("@#{name}") or name == :id
|
66
|
+
|
67
|
+
property = instance_variable_get("@#{name}")
|
68
|
+
property.respond_to?(:empty?) ? !property.empty? : !!property
|
69
|
+
end
|
70
|
+
|
71
|
+
# property=
|
72
|
+
define_method("#{name}=") do
|
73
|
+
# Check value type
|
74
|
+
type = opts[:type] || String
|
75
|
+
if opts[:multiple]
|
76
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
|
77
|
+
value.each do |val|
|
78
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
|
79
|
+
end
|
80
|
+
else
|
81
|
+
raise MetalArchives::Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
|
82
|
+
end
|
83
|
+
|
84
|
+
instance_variable_set name, value
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Defines a model enum property.
|
90
|
+
#
|
91
|
+
# [+name+]
|
92
|
+
# Name of the property
|
93
|
+
#
|
94
|
+
# [+opts+]
|
95
|
+
# [+values+]
|
96
|
+
# Required. An array of possible values
|
97
|
+
#
|
98
|
+
# [+multiple+]
|
99
|
+
# Whether or not the property has multiple values (which
|
100
|
+
# turns it into an +Array+ of +type+)
|
101
|
+
#
|
102
|
+
def enum(name, opts)
|
103
|
+
raise ArgumentError, 'opts[:values] is required' unless opts and opts[:values]
|
104
|
+
|
105
|
+
(@properties ||= []) << name
|
106
|
+
|
107
|
+
# property
|
108
|
+
define_method(name) do
|
109
|
+
self.fetch unless instance_variable_defined?("@#{name}")
|
110
|
+
instance_variable_get("@#{name}")
|
111
|
+
end
|
112
|
+
|
113
|
+
# property?
|
114
|
+
define_method("#{name}?") do
|
115
|
+
self.fetch unless instance_variable_defined?("@#{name}")
|
116
|
+
|
117
|
+
property = instance_variable_get("@#{name}")
|
118
|
+
property.respond_to?(:empty?) ? !property.empty? : !!property
|
119
|
+
end
|
120
|
+
|
121
|
+
# property=
|
122
|
+
define_method("#{name}=") do |value|
|
123
|
+
# Check enum type
|
124
|
+
if opts[:multiple]
|
125
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
|
126
|
+
value.each do |val|
|
127
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
|
128
|
+
end
|
129
|
+
else
|
130
|
+
raise MetalArchives::Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
|
131
|
+
end
|
132
|
+
|
133
|
+
instance_variable_set name, value
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
##
|
138
|
+
# Defines a model boolean property. This method is an alias for +enum name, :values => [true, false]+
|
139
|
+
#
|
140
|
+
# [+name+]
|
141
|
+
# Name of the property
|
142
|
+
#
|
143
|
+
# [+opts+]
|
144
|
+
# [+multiple+]
|
145
|
+
# Whether or not the property has multiple values (which
|
146
|
+
# turns it into an +Array+ of +type+)
|
147
|
+
#
|
148
|
+
def boolean(name, opts = {})
|
149
|
+
enum name, opts.merge(:values => [true, false])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'countries'
|
3
|
+
|
4
|
+
module MetalArchives
|
5
|
+
|
6
|
+
##
|
7
|
+
# Represents a record label
|
8
|
+
#
|
9
|
+
class Label < BaseModel
|
10
|
+
##
|
11
|
+
# :attr_reader: id
|
12
|
+
#
|
13
|
+
# Returns +Integer+
|
14
|
+
#
|
15
|
+
property :id
|
16
|
+
|
17
|
+
##
|
18
|
+
# :attr_reader: name
|
19
|
+
#
|
20
|
+
# Returns +String+
|
21
|
+
#
|
22
|
+
property :name
|
23
|
+
|
24
|
+
##
|
25
|
+
# :attr_reader: address
|
26
|
+
#
|
27
|
+
# Returns multiline +String+
|
28
|
+
#
|
29
|
+
property :address
|
30
|
+
|
31
|
+
##
|
32
|
+
# :attr_reader: country
|
33
|
+
#
|
34
|
+
# Returns +ISO316::Country+
|
35
|
+
#
|
36
|
+
property :country, :type => ISO3166::Country
|
37
|
+
|
38
|
+
##
|
39
|
+
# :attr_reader: phone
|
40
|
+
#
|
41
|
+
# Returns +String+
|
42
|
+
#
|
43
|
+
property :phone
|
44
|
+
|
45
|
+
##
|
46
|
+
# :attr_reader: specializations
|
47
|
+
#
|
48
|
+
# Returns +Array+ of +String+
|
49
|
+
#
|
50
|
+
property :specializations, :multiple => true
|
51
|
+
|
52
|
+
##
|
53
|
+
# :attr_reader: date_founded
|
54
|
+
#
|
55
|
+
# Returns +Date+
|
56
|
+
#
|
57
|
+
property :date_founded, :type => Date
|
58
|
+
|
59
|
+
##
|
60
|
+
# :attr_reader: sub_labels
|
61
|
+
#
|
62
|
+
# Returns +Array+ of rdoc-ref:Label
|
63
|
+
#
|
64
|
+
property :sub_labels, :type => MetalArchives::Label, :multiple => true
|
65
|
+
|
66
|
+
##
|
67
|
+
# :attr_reader: online_shopping
|
68
|
+
#
|
69
|
+
# Returns +Boolean+
|
70
|
+
#
|
71
|
+
boolean :online_shopping
|
72
|
+
|
73
|
+
##
|
74
|
+
# :attr_reader: contact
|
75
|
+
#
|
76
|
+
# Returns +Hash+ with the following keys: +title+, +content+
|
77
|
+
#
|
78
|
+
property :contact, :type => Hash, :multiple => true
|
79
|
+
|
80
|
+
##
|
81
|
+
# :attr_reader: status
|
82
|
+
#
|
83
|
+
# Returns +:active+, +:closed+ or +:unknown+
|
84
|
+
#
|
85
|
+
enum :status, :values => [:active, :closed, :unknown]
|
86
|
+
|
87
|
+
class << self
|
88
|
+
##
|
89
|
+
# Search by name.
|
90
|
+
#
|
91
|
+
# Returns +Array+ of rdoc-ref:Label
|
92
|
+
#
|
93
|
+
def search(name)
|
94
|
+
results = []
|
95
|
+
results
|
96
|
+
end
|
97
|
+
|
98
|
+
##
|
99
|
+
# Find by name and id.
|
100
|
+
#
|
101
|
+
# Returns rdoc-ref:Band
|
102
|
+
#
|
103
|
+
def find_by_name(name, id)
|
104
|
+
client.find_resource(
|
105
|
+
:band,
|
106
|
+
:name => name,
|
107
|
+
:id => id
|
108
|
+
)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|