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