metal_archives 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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