metal_archives 2.1.1 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/ci.yml +93 -0
  3. data/.gitignore +6 -6
  4. data/.overcommit.yml +35 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +69 -6
  7. data/CHANGELOG.md +29 -0
  8. data/Gemfile +1 -1
  9. data/LICENSE.md +17 -4
  10. data/README.md +65 -86
  11. data/Rakefile +8 -7
  12. data/bin/console +38 -0
  13. data/bin/setup +8 -0
  14. data/config/inflections.rb +7 -0
  15. data/config/initializers/.keep +0 -0
  16. data/docker-compose.yml +23 -0
  17. data/lib/metal_archives.rb +82 -25
  18. data/lib/metal_archives/cache/base.rb +40 -0
  19. data/lib/metal_archives/cache/memory.rb +68 -0
  20. data/lib/metal_archives/cache/null.rb +22 -0
  21. data/lib/metal_archives/cache/redis.rb +49 -0
  22. data/lib/metal_archives/{utils/collection.rb → collection.rb} +3 -5
  23. data/lib/metal_archives/configuration.rb +33 -50
  24. data/lib/metal_archives/{error.rb → errors.rb} +9 -1
  25. data/lib/metal_archives/http_client.rb +45 -44
  26. data/lib/metal_archives/models/artist.rb +90 -45
  27. data/lib/metal_archives/models/band.rb +80 -55
  28. data/lib/metal_archives/models/base.rb +218 -0
  29. data/lib/metal_archives/models/label.rb +14 -15
  30. data/lib/metal_archives/models/release.rb +349 -0
  31. data/lib/metal_archives/parsers/artist.rb +86 -50
  32. data/lib/metal_archives/parsers/band.rb +155 -88
  33. data/lib/metal_archives/parsers/base.rb +14 -0
  34. data/lib/metal_archives/parsers/country.rb +21 -0
  35. data/lib/metal_archives/parsers/date.rb +31 -0
  36. data/lib/metal_archives/parsers/genre.rb +67 -0
  37. data/lib/metal_archives/parsers/label.rb +39 -31
  38. data/lib/metal_archives/parsers/parser.rb +16 -63
  39. data/lib/metal_archives/parsers/release.rb +242 -0
  40. data/lib/metal_archives/parsers/year.rb +29 -0
  41. data/lib/metal_archives/version.rb +12 -1
  42. data/metal_archives.env.example +10 -0
  43. data/metal_archives.gemspec +43 -28
  44. data/nginx/default.conf +60 -0
  45. metadata +181 -72
  46. data/.travis.yml +0 -12
  47. data/lib/metal_archives/middleware/cache_check.rb +0 -20
  48. data/lib/metal_archives/middleware/encoding.rb +0 -16
  49. data/lib/metal_archives/middleware/headers.rb +0 -38
  50. data/lib/metal_archives/middleware/rewrite_endpoint.rb +0 -38
  51. data/lib/metal_archives/models/base_model.rb +0 -215
  52. data/lib/metal_archives/utils/lru_cache.rb +0 -61
  53. data/lib/metal_archives/utils/nil_date.rb +0 -99
  54. data/lib/metal_archives/utils/range.rb +0 -66
  55. data/spec/configuration_spec.rb +0 -96
  56. data/spec/factories/artist_factory.rb +0 -37
  57. data/spec/factories/band_factory.rb +0 -60
  58. data/spec/factories/nil_date_factory.rb +0 -9
  59. data/spec/factories/range_factory.rb +0 -8
  60. data/spec/models/artist_spec.rb +0 -138
  61. data/spec/models/band_spec.rb +0 -164
  62. data/spec/models/base_model_spec.rb +0 -219
  63. data/spec/parser_spec.rb +0 -19
  64. data/spec/spec_helper.rb +0 -111
  65. data/spec/support/factory_girl.rb +0 -5
  66. data/spec/support/metal_archives.rb +0 -33
  67. data/spec/utils/collection_spec.rb +0 -72
  68. data/spec/utils/lru_cache_spec.rb +0 -53
  69. data/spec/utils/nil_date_spec.rb +0 -156
  70. data/spec/utils/range_spec.rb +0 -62
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetalArchives
4
+ ##
5
+ # Abstract model class
6
+ #
7
+ class Base
8
+ ##
9
+ # Generic shallow copy constructor
10
+ #
11
+ def initialize(attributes = {})
12
+ raise Errors::NotImplementedError, "no :id property in model" unless respond_to? :id
13
+
14
+ set(**attributes)
15
+ end
16
+
17
+ ##
18
+ # Set properties
19
+ #
20
+ def set(**attributes)
21
+ attributes.each { |key, value| instance_variable_set(:"@#{key}", value) }
22
+ end
23
+
24
+ ##
25
+ # Returns true if two objects have the same type and id
26
+ #
27
+ def ==(other)
28
+ other.is_a?(self.class) &&
29
+ id == other.id
30
+ end
31
+
32
+ ##
33
+ # Fetch, parse and load the data
34
+ #
35
+ # [Raises]
36
+ # - rdoc-ref:Errors::InvalidIDError when no id
37
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
38
+ #
39
+ def load!
40
+ raise Errors::InvalidIDError, "no id present" unless id
41
+
42
+ # Use constructor to set attributes
43
+ set(**assemble)
44
+
45
+ @loaded = true
46
+ MetalArchives.cache[id] = self
47
+ rescue StandardError => e
48
+ # Don't cache invalid requests
49
+ MetalArchives.cache.delete id
50
+ raise e
51
+ end
52
+
53
+ ##
54
+ # Whether or not the object is currently loaded
55
+ #
56
+ def loaded?
57
+ @loaded ||= false
58
+ end
59
+
60
+ ##
61
+ # Whether or not the object is currently cached
62
+ #
63
+ def cached?
64
+ loaded? && MetalArchives.cache.include?(id)
65
+ end
66
+
67
+ ##
68
+ # String representation
69
+ #
70
+ def inspect
71
+ "#<#{self.class.name} @id=#{@id} @name=\"#{@name}\">"
72
+ end
73
+
74
+ protected
75
+
76
+ ##
77
+ # Fetch the data and assemble the model
78
+ #
79
+ # Override this method
80
+ #
81
+ # [Raises]
82
+ # - rdoc-ref:Errors::InvalidIDError when no or invalid id
83
+ # - rdoc-ref:Errors::APIError when receiving a status code >= 400 (except 404)
84
+ #
85
+ def assemble
86
+ raise Errors::NotImplementedError, "method :assemble not implemented"
87
+ end
88
+
89
+ class << self
90
+ ##
91
+ # Declared properties
92
+ #
93
+ def properties
94
+ @properties ||= {}
95
+ end
96
+
97
+ protected
98
+
99
+ ##
100
+ # Defines a model property.
101
+ #
102
+ # [+name+]
103
+ # Name of the property
104
+ #
105
+ # [+opts+]
106
+ # [+type+]
107
+ # Data type of property (a constant)
108
+ #
109
+ # Default: +String+
110
+ #
111
+ # [+multiple+]
112
+ # Whether or not the property has multiple values (which
113
+ # turns it into an +Array+ of +type+)
114
+ #
115
+ def property(name, opts = {})
116
+ properties[name] = opts
117
+
118
+ # property
119
+ define_method(name) do
120
+ # Load only when not loaded or id property
121
+ load! unless loaded? || name == :id
122
+
123
+ instance_variable_get(:"@#{name}")
124
+ end
125
+
126
+ # property?
127
+ define_method("#{name}?") do
128
+ send(name).present?
129
+ end
130
+
131
+ # property=
132
+ define_method("#{name}=") do |value|
133
+ return instance_variable_set(:"@#{name}", value) if value.nil?
134
+
135
+ # Check value type
136
+ type = opts[:type] || String
137
+ if opts[:multiple]
138
+ raise Errors::TypeError, "invalid type #{value.class}, must be Array for #{name}" unless value.is_a? Array
139
+
140
+ value.each do |val|
141
+ raise Errors::TypeError, "invalid type #{val.class}, must be #{type} for #{name}" unless val.is_a? type
142
+ end
143
+ else
144
+ raise Errors::TypeError, "invalid type #{value.class}, must be #{type} for #{name}" unless value.is_a? type
145
+ end
146
+
147
+ instance_variable_set(:"@#{name}", value)
148
+ end
149
+ end
150
+
151
+ ##
152
+ # Defines a model enum property.
153
+ #
154
+ # [+name+]
155
+ # Name of the property
156
+ #
157
+ # [+opts+]
158
+ # [+values+]
159
+ # Required. An array of possible values
160
+ #
161
+ # [+multiple+]
162
+ # Whether or not the property has multiple values (which
163
+ # turns it into an +Array+ of +type+)
164
+ #
165
+ def enum(name, opts)
166
+ raise ArgumentError, "opts[:values] is required" unless opts && opts[:values]
167
+
168
+ properties[name] = opts
169
+
170
+ # property
171
+ define_method(name) do
172
+ load! unless loaded?
173
+
174
+ instance_variable_get(:"@#{name}")
175
+ end
176
+
177
+ # property?
178
+ define_method("#{name}?") do
179
+ load! unless loaded? && instance_variable_defined?("@#{name}")
180
+
181
+ property = instance_variable_get(:"@#{name}")
182
+ property.respond_to?(:empty?) ? !property.empty? : !property.nil?
183
+ end
184
+
185
+ # property=
186
+ define_method("#{name}=") do |value|
187
+ # Check enum type
188
+ if opts[:multiple]
189
+ raise Errors::TypeError, "invalid enum value #{value}, must be Array for #{name}" unless value.is_a? Array
190
+
191
+ value.each do |val|
192
+ raise Errors::TypeError, "invalid enum value #{val} for #{name}" unless opts[:values].include? val
193
+ end
194
+ else
195
+ raise Errors::TypeError, "invalid enum value #{value} for #{name}" unless opts[:values].include? value
196
+ end
197
+
198
+ instance_variable_set(:"@#{name}", value)
199
+ end
200
+ end
201
+
202
+ ##
203
+ # Defines a model boolean property. This method is an alias for <tt>enum name, :values => [true, false]</tt>
204
+ #
205
+ # [+name+]
206
+ # Name of the property
207
+ #
208
+ # [+opts+]
209
+ # [+multiple+]
210
+ # Whether or not the property has multiple values (which
211
+ # turns it into an +Array+ of +type+)
212
+ #
213
+ def boolean(name, opts = {})
214
+ enum name, opts.merge(values: [true, false])
215
+ end
216
+ end
217
+ end
218
+ end
@@ -1,19 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'date'
4
- require 'countries'
3
+ require "date"
4
+ require "countries"
5
5
 
6
6
  module MetalArchives
7
7
  ##
8
8
  # Represents a record label
9
9
  #
10
- class Label < MetalArchives::BaseModel
10
+ class Label < Base
11
11
  ##
12
12
  # :attr_reader: id
13
13
  #
14
14
  # Returns +Integer+
15
15
  #
16
- property :id, :type => Integer
16
+ property :id, type: Integer
17
17
 
18
18
  ##
19
19
  # :attr_reader: name
@@ -46,7 +46,7 @@ module MetalArchives
46
46
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
47
47
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
48
48
  #
49
- property :country, :type => ISO3166::Country
49
+ property :country, type: ISO3166::Country
50
50
 
51
51
  ##
52
52
  # :attr_reader: phone
@@ -68,25 +68,25 @@ module MetalArchives
68
68
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
69
69
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
70
70
  #
71
- property :specializations, :multiple => true
71
+ property :specializations, multiple: true
72
72
 
73
73
  ##
74
74
  # :attr_reader: date_founded
75
75
  #
76
- # Returns +NilDate+
76
+ # Returns +Date+
77
77
  #
78
78
  # [Raises]
79
79
  # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
80
80
  # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
81
81
  #
82
- property :date_founded, :type => NilDate
82
+ property :date_founded, type: Date
83
83
 
84
84
  ##
85
85
  # :attr_reader: sub_labels
86
86
  #
87
87
  # Returns +Array+ of rdoc-ref:Label
88
88
  #
89
- property :sub_labels, :type => MetalArchives::Label, :multiple => true
89
+ property :sub_labels, type: Label, multiple: true
90
90
 
91
91
  ##
92
92
  # :attr_reader: online_shopping
@@ -100,14 +100,14 @@ module MetalArchives
100
100
  #
101
101
  # Returns +Hash+ with the following keys: +title+, +content+
102
102
  #
103
- property :contact, :type => Hash, :multiple => true
103
+ property :contact, type: Hash, multiple: true
104
104
 
105
105
  ##
106
106
  # :attr_reader: status
107
107
  #
108
108
  # Returns +:active+, +:closed+ or +:unknown+
109
109
  #
110
- enum :status, :values => %i[active closed unknown]
110
+ enum :status, values: [:active, :closed, :unknown]
111
111
 
112
112
  class << self
113
113
  ##
@@ -116,8 +116,7 @@ module MetalArchives
116
116
  # Returns +Array+ of rdoc-ref:Label
117
117
  #
118
118
  def search(_name)
119
- results = []
120
- results
119
+ []
121
120
  end
122
121
 
123
122
  ##
@@ -128,8 +127,8 @@ module MetalArchives
128
127
  def find_by_name(name, id)
129
128
  client.find_resource(
130
129
  :band,
131
- :name => name,
132
- :id => id
130
+ name: name,
131
+ id: id,
133
132
  )
134
133
  end
135
134
  end
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "nokogiri"
5
+
6
+ module MetalArchives
7
+ ##
8
+ # Represents a release
9
+ #
10
+ class Release < Base
11
+ ##
12
+ # :attr_reader: id
13
+ #
14
+ # Returns +Integer+
15
+ #
16
+ property :id, type: Integer
17
+
18
+ ##
19
+ # :attr_reader: title
20
+ #
21
+ # Returns +String+
22
+ #
23
+ # [Raises]
24
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
25
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
26
+ #
27
+ property :title
28
+
29
+ # TODO: band
30
+
31
+ ##
32
+ # :attr_reader: type
33
+ #
34
+ # Returns +:full_length+, +:live+, +:demo+, +:single+, +:ep+, +:video+, +:boxed_set+, +:split+, +:compilation+, +:split_video+, +:collaboration+
35
+ #
36
+ # [Raises]
37
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
38
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
39
+ #
40
+ enum :type, values: [:full_length, :live, :demo, :single, :ep, :video, :boxed_set, :split, :compilation, :split_video, :collaboration]
41
+
42
+ ##
43
+ # :attr_reader: date_released
44
+ #
45
+ # Returns rdoc-ref:Date
46
+ #
47
+ # [Raises]
48
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
49
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
50
+ #
51
+ property :date_released, type: Date
52
+
53
+ ##
54
+ # :attr_reader_: catalog_id
55
+ #
56
+ # Return +String+
57
+ #
58
+ # [Raises]
59
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
60
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
61
+ #
62
+ property :catalog_id
63
+
64
+ ##
65
+ # :attr_reader_: version_description
66
+ #
67
+ # Return +String+
68
+ #
69
+ # [Raises]
70
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
71
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
72
+ #
73
+ property :version_description
74
+
75
+ # TODO: label
76
+
77
+ ##
78
+ # :attr_reader: format
79
+ #
80
+ # Returns +:cd+, +:cassette+, +:vinyl+, +:vhs+, +:dvd+, +:digital+, +:blu_ray+, +:other+, +:unknown+
81
+ #
82
+ # [Raises]
83
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
84
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
85
+ #
86
+ property :format
87
+
88
+ ##
89
+ # :attr_reader: limitation
90
+ #
91
+ # Returns +Integer+
92
+ #
93
+ # [Raises]
94
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
95
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
96
+ #
97
+ property :limitation
98
+
99
+ # TODO: reviews
100
+ # TODO: songs
101
+ # TODO: lineup
102
+ # TODO: other versions
103
+ # TODO: links
104
+
105
+ ##
106
+ # :attr_reader: notes
107
+ #
108
+ # Returns raw HTML +String+
109
+ #
110
+ # [Raises]
111
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
112
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
113
+ #
114
+ property :notes
115
+
116
+ protected
117
+
118
+ ##
119
+ # Fetch the data and assemble the model
120
+ #
121
+ # [Raises]
122
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when receiving a status code == 404
123
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
124
+ #
125
+ def assemble # :nodoc:
126
+ ## Base attributes
127
+ response = MetalArchives.http.get "/albums/view/id/#{id}"
128
+
129
+ Parsers::Release.parse_html response.to_s
130
+ end
131
+
132
+ class << self
133
+ ##
134
+ # Find by ID
135
+ #
136
+ # Returns rdoc-ref:Release, even when ID is invalid (because the data is lazily fetched)
137
+ #
138
+ # [+id+]
139
+ # +Integer+
140
+ #
141
+ def find(id)
142
+ return MetalArchives.cache[id] if MetalArchives.cache.include? id
143
+
144
+ Release.new id: id
145
+ end
146
+
147
+ ##
148
+ # Find by ID (no lazy loading)
149
+ #
150
+ # Returns rdoc-ref:Release
151
+ #
152
+ # [Raises]
153
+ # - rdoc-ref:MetalArchives::Errors::InvalidIDError when no or invalid id
154
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400 (except 404)
155
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
156
+ #
157
+ # [+id+]
158
+ # +Integer+
159
+ #
160
+ def find!(id)
161
+ obj = find id
162
+ obj.load! if obj && !obj.loaded?
163
+
164
+ obj
165
+ end
166
+
167
+ ##
168
+ # Find by attributes
169
+ #
170
+ # Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
171
+ #
172
+ # Returns rdoc-ref:Release or nil when no results
173
+ #
174
+ # [Raises]
175
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400
176
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
177
+ #
178
+ # [+query+]
179
+ # Hash containing one or more of the following keys:
180
+ # - +:band_name+: +String+
181
+ # - +:title+: +String+
182
+ # - +:from_year+: +Integer+
183
+ # - +:from_month+: +Integer+
184
+ # - +:to_year+: +Integer+
185
+ # - +:to_month+: +Integer+
186
+ # - +:country+: +ISO3166::Country+ or +Array+ of ISO3166::Country
187
+ # - +:location+: +String+
188
+ # - +:label_name+: +String+
189
+ # - +:indie+: +Boolean+
190
+ # - +:catalog_id+: +String+
191
+ # - +:identifier+: +String+, identifier (barcode, matrix, etc.)
192
+ # - +:recording_info+: +String+, recording information (studio, city, etc.)
193
+ # - +:version_description+: +String+, version description (country, digipak, etc.)
194
+ # - +:notes+: +String+
195
+ # - +:genre+: +String+
196
+ # - +:types+: +Array+ of +Symbol+, see rdoc-ref:Release.type
197
+ # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
198
+ #
199
+ def find_by(query)
200
+ params = Parsers::Release.map_params query
201
+
202
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params
203
+ json = JSON.parse response.to_s
204
+
205
+ return nil if json["aaData"].empty?
206
+
207
+ data = json["aaData"].first
208
+ id = Nokogiri::HTML(data[1]).xpath("//a/@href").first.value.delete('\\').split("/").last.gsub(/\D/, "").to_i
209
+
210
+ find id
211
+ end
212
+
213
+ ##
214
+ # Find by attributes (no lazy loading)
215
+ #
216
+ # Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
217
+ #
218
+ # Returns rdoc-ref:Release or nil when no results
219
+ #
220
+ # [Raises]
221
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400
222
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
223
+ #
224
+ # [+query+]
225
+ # Hash containing one or more of the following keys:
226
+ # - +:band_name+: +String+
227
+ # - +:title+: +String+
228
+ # - +:from_year+: +Integer+
229
+ # - +:from_month+: +Integer+
230
+ # - +:to_year+: +Integer+
231
+ # - +:to_month+: +Integer+
232
+ # - +:country+: +ISO3166::Country+ or +Array+ of ISO3166::Country
233
+ # - +:location+: +String+
234
+ # - +:label_name+: +String+
235
+ # - +:indie+: +Boolean+
236
+ # - +:catalog_id+: +String+
237
+ # - +:identifier+: +String+, identifier (barcode, matrix, etc.)
238
+ # - +:recording_info+: +String+, recording information (studio, city, etc.)
239
+ # - +:version_description+: +String+, version description (country, digipak, etc.)
240
+ # - +:notes+: +String+
241
+ # - +:genre+: +String+
242
+ # - +:types+: +Array+ of +Symbol+, see rdoc-ref:Release.type
243
+ # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
244
+ #
245
+ def find_by!(query)
246
+ obj = find_by query
247
+ obj.load! if obj && !obj.loaded?
248
+
249
+ obj
250
+ end
251
+
252
+ ##
253
+ # Search by attributes
254
+ #
255
+ # Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
256
+ #
257
+ # Returns rdoc-ref:Collection of rdoc-ref:Release
258
+ #
259
+ # [Raises]
260
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400
261
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
262
+ #
263
+ # [+query+]
264
+ # Hash containing one or more of the following keys:
265
+ # - +:band_name+: +String+
266
+ # - +:title+: +String+
267
+ # - +:from_year+: +Integer+
268
+ # - +:from_month+: +Integer+
269
+ # - +:to_year+: +Integer+
270
+ # - +:to_month+: +Integer+
271
+ # - +:country+: +ISO3166::Country+ or +Array+ of ISO3166::Country
272
+ # - +:location+: +String+
273
+ # - +:label_name+: +String+
274
+ # - +:indie+: +Boolean+
275
+ # - +:catalog_id+: +String+
276
+ # - +:identifier+: +String+, identifier (barcode, matrix, etc.)
277
+ # - +:recording_info+: +String+, recording information (studio, city, etc.)
278
+ # - +:version_description+: +String+, version description (country, digipak, etc.)
279
+ # - +:notes+: +String+
280
+ # - +:genre+: +String+
281
+ # - +:types+: +Array+ of +Symbol+, see rdoc-ref:Release.type
282
+ # - +:formats+: +Array+ of +Symbol+, see rdoc-ref:Release.format
283
+ #
284
+ def search_by(query)
285
+ params = Parsers::Release.map_params query
286
+
287
+ l = lambda do
288
+ @start ||= 0
289
+
290
+ if @max_items && @start >= @max_items
291
+ []
292
+ else
293
+ response = MetalArchives.http.get "/search/ajax-advanced/searching/albums", params.merge(iDisplayStart: @start)
294
+ json = JSON.parse response.to_s
295
+
296
+ @max_items = json["iTotalRecords"]
297
+
298
+ objects = []
299
+
300
+ json["aaData"].each do |data|
301
+ # Create Release object for every ID in the results list
302
+ id = Nokogiri::HTML(data.first).xpath("//a/@href").first.value.delete('\\').split("/").last.gsub(/\D/, "").to_i
303
+ objects << Release.find(id)
304
+ end
305
+
306
+ @start += 200
307
+
308
+ objects
309
+ end
310
+ end
311
+
312
+ MetalArchives::Collection.new l
313
+ end
314
+
315
+ ##
316
+ # Search by title, resolves to rdoc-ref:Release.search_by <tt>(:title => title)</tt>
317
+ #
318
+ # Refer to {MA's FAQ}[http://www.metal-archives.com/content/help?index=3#tab_db] for search tips.
319
+ #
320
+ # Returns (possibly empty) +Array+ of rdoc-ref:Release
321
+ #
322
+ # [Raises]
323
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400
324
+ # - rdoc-ref:MetalArchives::Errors::ArgumentError when +title+ isn't a +String+
325
+ #
326
+ # [+title+]
327
+ # +String+
328
+ #
329
+ def search(title)
330
+ raise MetalArchives::Errors::ArgumentError unless title.is_a? String
331
+
332
+ search_by title: title
333
+ end
334
+
335
+ ##
336
+ # Get all releases
337
+ #
338
+ # Returns rdoc-ref:Collection of rdoc-ref:Release
339
+ #
340
+ # [Raises]
341
+ # - rdoc-ref:MetalArchives::Errors::APIError when receiving a status code >= 400
342
+ # - rdoc-ref:MetalArchives::Errors::ParserError when parsing failed. Please report this error.
343
+ #
344
+ def all
345
+ search ""
346
+ end
347
+ end
348
+ end
349
+ end