rbrainz 0.1.1 → 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.
Files changed (85) hide show
  1. data/CHANGES +31 -0
  2. data/LICENSE +1 -1
  3. data/README +3 -2
  4. data/Rakefile +40 -22
  5. data/TODO +6 -23
  6. data/doc/README.rdoc +50 -21
  7. data/examples/getartist.rb +6 -4
  8. data/examples/getuser.rb +30 -0
  9. data/examples/searchartists.rb +35 -0
  10. data/lib/rbrainz.rb +12 -7
  11. data/lib/rbrainz/core_ext.rb +8 -0
  12. data/lib/rbrainz/core_ext/mbid.rb +30 -0
  13. data/lib/rbrainz/core_ext/net_http_digest.rb +52 -0
  14. data/lib/rbrainz/core_ext/range.rb +28 -0
  15. data/lib/rbrainz/core_ext/range/equality.rb +232 -0
  16. data/lib/rbrainz/data/countrynames.rb +7 -5
  17. data/lib/rbrainz/data/languagenames.rb +8 -5
  18. data/lib/rbrainz/data/releasetypenames.rb +34 -0
  19. data/lib/rbrainz/data/scriptnames.rb +8 -5
  20. data/lib/rbrainz/model.rb +27 -35
  21. data/lib/rbrainz/model/alias.rb +31 -7
  22. data/lib/rbrainz/model/artist.rb +30 -41
  23. data/lib/rbrainz/model/collection.rb +102 -0
  24. data/lib/rbrainz/model/default_factory.rb +78 -0
  25. data/lib/rbrainz/model/disc.rb +45 -8
  26. data/lib/rbrainz/model/entity.rb +122 -53
  27. data/lib/rbrainz/model/incomplete_date.rb +31 -47
  28. data/lib/rbrainz/model/individual.rb +103 -0
  29. data/lib/rbrainz/model/label.rb +42 -33
  30. data/lib/rbrainz/model/mbid.rb +111 -40
  31. data/lib/rbrainz/model/relation.rb +78 -14
  32. data/lib/rbrainz/model/release.rb +119 -31
  33. data/lib/rbrainz/model/release_event.rb +38 -9
  34. data/lib/rbrainz/model/scored_collection.rb +99 -0
  35. data/lib/rbrainz/model/tag.rb +39 -0
  36. data/lib/rbrainz/model/track.rb +37 -13
  37. data/lib/rbrainz/model/user.rb +48 -0
  38. data/lib/rbrainz/utils.rb +9 -0
  39. data/lib/rbrainz/utils/data.rb +78 -0
  40. data/lib/rbrainz/utils/helper.rb +22 -0
  41. data/lib/rbrainz/version.rb +15 -0
  42. data/lib/rbrainz/webservice.rb +32 -6
  43. data/lib/rbrainz/webservice/filter.rb +124 -47
  44. data/lib/rbrainz/webservice/includes.rb +49 -10
  45. data/lib/rbrainz/webservice/mbxml.rb +228 -173
  46. data/lib/rbrainz/webservice/query.rb +312 -25
  47. data/lib/rbrainz/webservice/webservice.rb +164 -27
  48. data/test/lib/mock_webservice.rb +53 -0
  49. data/test/lib/test_entity.rb +27 -8
  50. data/test/lib/test_factory.rb +47 -0
  51. data/test/lib/testing_helper.rb +7 -5
  52. data/test/test-data/invalid/artist/tags_1.xml +6 -0
  53. data/test/test-data/valid/artist/Tchaikovsky-2.xml +12 -0
  54. data/test/test-data/valid/label/Atlantic_Records_2.xml +3 -0
  55. data/test/test-data/valid/label/Atlantic_Records_3.xml +11 -0
  56. data/test/test-data/valid/release/Highway_61_Revisited_2.xml +12 -0
  57. data/test/test-data/valid/track/Silent_All_These_Years_6.xml +8 -0
  58. data/test/test_alias.rb +13 -7
  59. data/test/test_artist.rb +26 -4
  60. data/test/test_artist_filter.rb +11 -6
  61. data/test/test_artist_includes.rb +11 -6
  62. data/test/test_collection.rb +66 -0
  63. data/test/test_default_factory.rb +75 -0
  64. data/test/test_disc.rb +9 -4
  65. data/test/test_incomplete_date.rb +21 -14
  66. data/test/test_label.rb +56 -18
  67. data/test/test_label_filter.rb +10 -5
  68. data/test/test_label_includes.rb +11 -6
  69. data/test/test_mbid.rb +34 -19
  70. data/test/test_mbxml.rb +242 -72
  71. data/test/test_query.rb +92 -7
  72. data/test/test_range_equality.rb +144 -0
  73. data/test/test_relation.rb +18 -7
  74. data/test/test_release.rb +15 -4
  75. data/test/test_release_event.rb +16 -4
  76. data/test/test_release_filter.rb +11 -5
  77. data/test/test_release_includes.rb +11 -6
  78. data/test/test_scored_collection.rb +86 -0
  79. data/test/test_tag.rb +39 -0
  80. data/test/test_track.rb +15 -4
  81. data/test/test_track_filter.rb +11 -5
  82. data/test/test_track_includes.rb +11 -6
  83. data/test/test_utils.rb +41 -0
  84. data/test/test_webservice.rb +16 -17
  85. metadata +93 -57
@@ -1,7 +1,9 @@
1
- # $Id: query.rb 31 2007-05-29 02:38:42Z phw $
2
- # Copyright (c) 2007, Philipp Wolfer
3
- # All rights reserved.
4
- # See LICENSE for permissions.
1
+ # $Id: query.rb 147 2007-07-19 17:10:26Z phw $
2
+ #
3
+ # Author:: Philipp Wolfer (mailto:phw@rubyforge.org)
4
+ # Copyright:: Copyright (c) 2007, Nigel Graham, Philipp Wolfer
5
+ # License:: RBrainz is free software distributed under a BSD style license.
6
+ # See LICENSE[file:../LICENSE.html] for permissions.
5
7
 
6
8
  require 'rbrainz/webservice/webservice'
7
9
  require 'rbrainz/webservice/mbxml'
@@ -9,54 +11,339 @@ require 'rbrainz/webservice/mbxml'
9
11
  module MusicBrainz
10
12
  module Webservice
11
13
 
14
+ # A simple interface to the MusicBrainz web service.
15
+ #
16
+ # This is a facade which provides a simple interface to the MusicBrainz
17
+ # web service. It hides all the details like fetching data from a server,
18
+ # parsing the XML and creating an object tree. Using this class, you can
19
+ # request data by ID or search the _collection_ of all resources
20
+ # (artists, labels, releases or tracks) to retrieve those matching given
21
+ # criteria. This document contains examples to get you started.
22
+ #
23
+ #
24
+ # == Working with Identifiers
25
+ # MusicBrainz uses absolute URIs as identifiers. For example, the artist
26
+ # 'Tori Amos' is identified using the following URI:
27
+ # http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5
28
+ #
29
+ # In some situations it is obvious from the context what type of
30
+ # resource an ID refers to. In these cases, abbreviated identifiers may
31
+ # be used, which are just the _UUID_ part of the URI. Thus the ID above
32
+ # may also be written like this:
33
+ # c0b2500e-0cef-4130-869d-732b23ed9df5
34
+ #
35
+ # All methods in this class which require IDs accept both the absolute
36
+ # URI and the abbreviated form (aka the relative URI).
37
+ #
38
+ #
39
+ # == Creating a Query Object
40
+ #
41
+ # In most cases, creating a Query object is as simple as this:
42
+ # require 'rbrainz'
43
+ # q = MusicBrainz::Webservice::Query.new
44
+ #
45
+ # The instantiated object uses the standard Webservice class to
46
+ # access the MusicBrainz web service. If you want to use a different
47
+ # server or you have to pass user name and password because one of
48
+ # your queries requires authentication, you have to create the
49
+ # Webservice object yourself and configure it appropriately.
50
+ #
51
+ # This example uses the MusicBrainz test server and also sets
52
+ # authentication data:
53
+ # require 'rbrainz'
54
+ # service = MusicBrainz::Webservice::Webservice.new(
55
+ # :host => 'test.musicbrainz.org',
56
+ # :username => 'whatever',
57
+ # :password => 'secret'
58
+ # )
59
+ # q = MusicBrainz::Webservice::Query.new(service)
60
+ #
61
+ #
62
+ # == Querying for Individual Resources
63
+ #
64
+ # If the MusicBrainz ID of a resource is known, then the get_artist_by_id,
65
+ # get_label_by_id, get_release_by_id or get_track_by_id method can be used
66
+ # to retrieve it.
67
+ #
68
+ # Example:
69
+ # require 'rbrainz'
70
+ # q = MusicBrainz::Webservice::Query.new
71
+ # artist = q.get_artist_by_id('c0b2500e-0cef-4130-869d-732b23ed9df5')
72
+ # puts artist.name
73
+ # puts artist.sort_name
74
+ # puts artist.type
75
+ #
76
+ # _produces_:
77
+ # Tori Amos
78
+ # Amos, Tori
79
+ # http://musicbrainz.org/ns/mmd-1.0#Person
80
+ #
81
+ # This returned just the basic artist data, however. To get more detail
82
+ # about a resource, the _includes_ parameters may be used
83
+ # which expect an ArtistIncludes, ReleaseIncludes, TrackIncludes or
84
+ # LabelIncludes object, depending on the resource type.
85
+ # It is also possible to use a Hash for the _includes_ parameter where it
86
+ # will then get automaticly wrapped in the appropriate includes class.
87
+ #
88
+ # To get data about a release which also includes the main artist
89
+ # and all tracks, for example, the following query can be used:
90
+ # require 'rbrainz'
91
+ # q = MusicBrainz::Webservice::Query.new
92
+ # release_id = '33dbcf02-25b9-4a35-bdb7-729455f33ad7'
93
+ # release = q.get_release_by_id(release_id, :artist=>true, :tracks=>true)
94
+ # puts release.title
95
+ # puts release.artist.name
96
+ # puts release.tracks[0].title
97
+ #
98
+ # _produces_:
99
+ # Tales of a Librarian
100
+ # Tori Amos
101
+ # Precious Things
102
+ #
103
+ # Note that the query gets more expensive for the server the more
104
+ # data you request, so please be nice.
105
+ #
106
+ #
107
+ # == Searching in Collections
108
+ #
109
+ # For each resource type (artist, release, and track), there is one
110
+ # collection which contains all resources of a type. You can search
111
+ # these collections using the get_artists, get_releases, and
112
+ # get_tracks methods. The collections are huge, so you have to
113
+ # use filters (ArtistFilter, ReleaseFilter, TrackFilter or LabelFilter)
114
+ # to retrieve only resources matching given criteria.
115
+ # As with includes it is also possible to use a Hash as the filter where
116
+ # it will then get wrapped in the appropriate filter class.
117
+ #
118
+ # For example, If you want to search the release collection for
119
+ # releases with a specified DiscID, you would use get_releases:
120
+ # require 'rbrainz'
121
+ # q = MusicBrainz::Webservice::Query.new
122
+ # results = q.get_releases(ReleaseFilter.new(:disc_id=>discId='8jJklE258v6GofIqDIrE.c5ejBE-'))
123
+ # puts results[0].score
124
+ # puts results[0].entity.title
125
+ #
126
+ # _produces_:
127
+ # 100
128
+ # Under the Pink
129
+ #
130
+ #
131
+ # The query returns a list of results in a ScoredCollection,
132
+ # which orders entities by score, with a higher score
133
+ # indicating a better match. Note that those results don't contain
134
+ # all the data about a resource. If you need more detail, you can then
135
+ # use the get_artist_by_id, get_label_by_id, get_release_by_id, or
136
+ # get_track_by_id methods to request the resource.
137
+ #
138
+ # All filters support the _limit_ argument to limit the number of
139
+ # results returned. This defaults to 25, but the server won't send
140
+ # more than 100 results to save bandwidth and processing power. In case
141
+ # you want to retrieve results above the 100 results limit you can use the
142
+ # _offset_ argument in the filters. The _offset_ specifies how many entries
143
+ # at the beginning of the collection should be skipped.
144
+ #
12
145
  class Query
13
146
 
14
- def initialize(webservice = nil)
147
+ # Create a new Query object.
148
+ #
149
+ # You can pass a custom Webservice[link:classes/MusicBrainz/Webservice/Webservice.html]
150
+ # object. If none is given a default webservice will be used.
151
+ #
152
+ # If the constructor is called without arguments, an instance
153
+ # of WebService is used, preconfigured to use the MusicBrainz
154
+ # server. This should be enough for most users.
155
+ #
156
+ # If you want to use queries which require authentication you
157
+ # have to pass a Webservice instance where user name and
158
+ # password have been set.
159
+ #
160
+ # The _client_id_ option is required for data submission.
161
+ # The format is <em>'application-version'</em>, where _application_
162
+ # is your application's name and _version_ is a version
163
+ # number which may not include a '-' character.
164
+ #
165
+ # Available options:
166
+ # [:client_id] a unicode string containing the application's ID.
167
+ # [:factory] A model factory. An instance of Model::DefaultFactory
168
+ # will be used if none is given.
169
+ def initialize(webservice = nil, options={ :client_id=>nil, :factory=>nil })
170
+ Utils.check_options options, :client_id, :factory
15
171
  @webservice = webservice.nil? ? Webservice.new : webservice
172
+ @client_id = options[:client_id] ? options[:client_id] : nil
173
+ @factory = options[:factory] ? options[:factory] : nil
16
174
  end
17
175
 
18
- def get_artist_by_id(id, include = nil)
19
- return get_entity_by_id(Model::Artist.entity_type, id, include)
176
+ # Returns an artist.
177
+ #
178
+ # The parameter _includes_ must be an instance of ArtistIncludes
179
+ # or a options hash as expected by ArtistIncludes.
180
+ #
181
+ # If no artist with that ID can be found, include contains invalid tags
182
+ # or there's a server problem, and exception is raised.
183
+ #
184
+ # Raises:: ConnectionError, RequestError, ResourceNotFoundError, ResponseError
185
+ def get_artist_by_id(id, includes = nil)
186
+ includes = ArtistIncludes.new(includes) unless includes.nil? || includes.is_a?(ArtistIncludes)
187
+ return get_entity_by_id(Model::Artist.entity_type, id, includes)
20
188
  end
21
189
 
22
- # TODO: implement
190
+ # Returns artists matching given criteria.
191
+ #
192
+ # The parameter _filter_ must be an instance of ArtistFilter
193
+ # or a options hash as expected by ArtistFilter.
194
+ #
195
+ # Raises:: ConnectionError, RequestError, ResponseError
23
196
  def get_artists(filter)
24
- raise NotImplementedError.new
197
+ filter = ArtistFilter.new(filter) unless filter.nil? || filter.is_a?(ArtistFilter)
198
+ return get_entities(Model::Artist.entity_type, filter)
25
199
  end
26
200
 
27
- def get_release_by_id(id, include = nil)
28
- return get_entity_by_id(Model::Release.entity_type, id, include)
201
+ # Returns an release.
202
+ #
203
+ # The parameter _includes_ must be an instance of ReleaseIncludes
204
+ # or a options hash as expected by ReleaseIncludes.
205
+ #
206
+ # If no release with that ID can be found, include contains invalid tags
207
+ # or there's a server problem, and exception is raised.
208
+ #
209
+ # Raises:: ConnectionError, RequestError, ResourceNotFoundError, ResponseError
210
+ def get_release_by_id(id, includes = nil)
211
+ includes = ReleaseIncludes.new(includes) unless includes.nil? || includes.is_a?(ReleaseIncludes)
212
+ return get_entity_by_id(Model::Release.entity_type, id, includes)
29
213
  end
30
214
 
31
- # TODO: implement
215
+ # Returns releases matching given criteria.
216
+ #
217
+ # The parameter _filter_ must be an instance of ReleaseFilter
218
+ # or a options hash as expected by ReleaseFilter.
219
+ #
220
+ # Raises:: ConnectionError, RequestError, ResponseError
32
221
  def get_releases(filter)
33
- raise NotImplementedError.new
222
+ filter = ReleaseFilter.new(filter) unless filter.nil? || filter.is_a?(ReleaseFilter)
223
+ return get_entities(Model::Release.entity_type, filter)
34
224
  end
35
225
 
36
- def get_track_by_id(id, include = nil)
37
- return get_entity_by_id(Model::Track.entity_type, id, include)
226
+ # Returns an track.
227
+ #
228
+ # The parameter _includes_ must be an instance of TrackIncludes
229
+ # or a options hash as expected by TrackIncludes.
230
+ #
231
+ # If no track with that ID can be found, include contains invalid tags
232
+ # or there's a server problem, and exception is raised.
233
+ #
234
+ # Raises:: ConnectionError, RequestError, ResourceNotFoundError, ResponseError
235
+ def get_track_by_id(id, includes = nil)
236
+ includes = TrackIncludes.new(includes) unless includes.nil? || includes.is_a?(TrackIncludes)
237
+ return get_entity_by_id(Model::Track.entity_type, id, includes)
38
238
  end
39
239
 
40
- # TODO: implement
240
+ # Returns tracks matching given criteria.
241
+ #
242
+ # The parameter _filter_ must be an instance of TrackFilter
243
+ # or a options hash as expected by TrackFilter.
244
+ #
245
+ # Raises:: ConnectionError, RequestError, ResponseError
41
246
  def get_tracks(filter)
42
- raise NotImplementedError.new
247
+ filter = TrackFilter.new(filter) unless filter.nil? || filter.is_a?(TrackFilter)
248
+ return get_entities(Model::Track.entity_type, filter)
43
249
  end
44
250
 
45
- def get_label_by_id(id, include = nil)
46
- return get_entity_by_id(Model::Label.entity_type, id, include)
251
+ # Returns an label.
252
+ #
253
+ # The parameter _includes_ must be an instance of LabelIncludes
254
+ # or a options hash as expected by LabelIncludes.
255
+ #
256
+ # If no label with that ID can be found, include contains invalid tags
257
+ # or there's a server problem, and exception is raised.
258
+ #
259
+ # Raises:: ConnectionError, RequestError, ResourceNotFoundError, ResponseError
260
+ def get_label_by_id(id, includes = nil)
261
+ includes = LabelIncludes.new(includes) unless includes.nil? || includes.is_a?(LabelIncludes)
262
+ return get_entity_by_id(Model::Label.entity_type, id, includes)
47
263
  end
48
264
 
49
- # TODO: implement
265
+ # Returns labels matching given criteria.
266
+ #
267
+ # The parameter _filter_ must be an instance of LabelFilter
268
+ # or a options hash as expected by LabelFilter.
269
+ #
270
+ # Raises:: ConnectionError, RequestError, ResponseError
50
271
  def get_labels(filter)
51
- raise NotImplementedError.new
272
+ filter = LabelFilter.new(filter) unless filter.nil? || filter.is_a?(LabelFilter)
273
+ return get_entities(Model::Label.entity_type, filter)
52
274
  end
53
275
 
54
- private
276
+ # Returns information about a MusicBrainz user.
277
+ #
278
+ # You can only request user data if you know the user name and password
279
+ # for that account. If username and/or password are incorrect, an
280
+ # AuthenticationError is raised.
281
+ #
282
+ # See the example in Query on how to supply user name and password.
283
+ #
284
+ # Raises:: ConnectionError, RequestError, AuthenticationError, ResponseError
285
+ def get_user_by_name(name)
286
+ xml = @webservice.get(:user, :filter => UserFilter.new(name))
287
+ collection = MBXML.new(xml).get_entity_list(:user, Model::NS_EXT_1)
288
+ unless collection and collection.size > 0
289
+ raise ResponseError("response didn't contain user data")
290
+ else
291
+ return collection[0].entity
292
+ end
293
+ end
294
+
295
+ # Submit track to PUID mappings.
296
+ #
297
+ # The <em>tracks2puids</em> parameter has to be a dictionary, with the
298
+ # keys being MusicBrainz track IDs (either as absolute URIs or
299
+ # in their 36 character ASCII representation) and the values
300
+ # being PUIDs (ASCII, 36 characters).
301
+ #
302
+ # Note that this method only works if a valid user name and
303
+ # password have been set. If username and/or password are incorrect,
304
+ # an AuthenticationError is raised. See the example in Query on
305
+ # how to supply authentication data.
306
+ #
307
+ # Raises:: ConnectionError, RequestError, AuthenticationError
308
+ def submit_puids(tracks2puids)
309
+ raise RequestError, 'Please supply a client ID' unless @client_id
310
+ params = [['client', @client_id.to_s]] # Encoded as utf-8
311
+
312
+ tracks2puids.each {|track_id, puid| params << ['puid', track_id + ' ' + puid ]}
313
+
314
+ @webservice.post('track', :querystring=>params)
315
+ end
316
+
317
+ private # ----------------------------------------------------------------
55
318
 
56
319
  # Helper method which will return any entity by ID.
57
- def get_entity_by_id(entity_type, id, include)
58
- xml = @webservice.get(entity_type, id, :include => include)
59
- return MBXML.new(xml).get_entity(entity_type)
320
+ #
321
+ # Raises:: ConnectionError, RequestError, ResourceNotFoundError, ResponseError
322
+ def get_entity_by_id(entity_type, id, includes)
323
+ stream = @webservice.get(entity_type, :id => id, :include => includes)
324
+ begin
325
+ entity = MBXML.new(stream, @factory).get_entity(entity_type)
326
+ rescue MBXML::ParseError => e
327
+ raise ResponseError.new(e.to_s)
328
+ end
329
+ unless entity
330
+ raise ResponseError.new("server didn't return #{entity_type.to_s} with the MBID #{id.to_s}")
331
+ else
332
+ return entity
333
+ end
334
+ end
335
+
336
+ # Helper method which will search for the given entity type.
337
+ #
338
+ # Raises:: ConnectionError, RequestError, ResponseError
339
+ def get_entities(entity_type, filter)
340
+ stream = @webservice.get(entity_type, :filter => filter)
341
+ begin
342
+ collection = MBXML.new(stream, @factory).get_entity_list(entity_type)
343
+ rescue MBXML::ParseError => e
344
+ raise ResponseError.new(e.to_s)
345
+ end
346
+ return collection
60
347
  end
61
348
 
62
349
  end
@@ -1,54 +1,107 @@
1
- # $Id: webservice.rb 36 2007-05-29 18:43:36Z phw $
2
- # Copyright (c) 2007, Philipp Wolfer
3
- # All rights reserved.
4
- # See LICENSE for permissions.
1
+ # $Id: webservice.rb 148 2007-07-19 17:26:33Z phw $
2
+ #
3
+ # Author:: Philipp Wolfer (mailto:phw@rubyforge.org)
4
+ # Copyright:: Copyright (c) 2007, Nigel Graham, Philipp Wolfer
5
+ # License:: RBrainz is free software distributed under a BSD style license.
6
+ # See LICENSE[file:../LICENSE.html] for permissions.
5
7
 
6
8
  require 'rbrainz/webservice/includes'
7
9
  require 'rbrainz/webservice/filter'
8
10
  require 'net/http'
11
+ require 'stringio'
9
12
 
10
13
  module MusicBrainz
11
14
  module Webservice
12
15
 
16
+ # An interface all concrete web service classes have to implement.
17
+ #
18
+ # All web service classes have to implement this and follow the method
19
+ # specifications.
13
20
  class IWebservice
14
21
 
15
- # Query the Webservice with HTTP GET.
16
- # Must be implemented by the concrete webservices.
17
- def get(entity, id, options = {:include => nil, :filter => nil, :version => 1})
18
- raise Exception.new('Called abstract method.')
22
+ # Query the web service.
23
+ #
24
+ # This method must be implemented by the concrete webservices and
25
+ # should return an IO object on success.
26
+ #
27
+ # Options:
28
+ # [:id] A MBID if querying for a single ressource.
29
+ # [:include] An include object (see AbstractIncludes).
30
+ # [:filter] A filter object (see AbstractFilter).
31
+ # [:version] The version of the webservice to use. Defaults to 1.
32
+ def get(entity_type, options={ :id=>nil, :include=>nil, :filter=>nil, :version=>1 })
33
+ raise NotImplementedError.new('Called abstract method.')
19
34
  end
20
35
 
21
- # Query the Webservice with HTTP POST.
22
- # Must be implemented by the concrete webservices.
23
- # TODO: Specify and implement in Webservice.
24
- def post
36
+ # Submit data to the web service.
37
+ #
38
+ # This method must be implemented by the concrete webservices and
39
+ # should return an IO object on success.
40
+ #
41
+ # Options:
42
+ # [:id] A MBID if querying for a single ressource.
43
+ # [:querystring] A string containing the data to post.
44
+ # [:version] The version of the webservice to use. Defaults to 1.
45
+ def post(entity_type, options={ :id=>nil, :querystring=>[], :version=>1 })
25
46
  raise NotImplementedError.new('Called abstract method.')
26
47
  end
27
48
 
28
49
  end
29
50
 
30
- # Webservice class to query the default MusicBrainz server.
31
- # TODO: Implement authorization.
51
+ # An interface to the MusicBrainz XML web service via HTTP.
52
+ #
53
+ # By default, this class uses the MusicBrainz server but may be configured
54
+ # for accessing other servers as well using the constructor. This implements
55
+ # IWebService, so additional documentation on method parameters can be found
56
+ # there.
32
57
  class Webservice < IWebservice
33
58
 
34
59
  # Timeouts for opening and reading connections (in seconds)
35
60
  attr_accessor :open_timeout, :read_timeout
36
61
 
37
- def initialize(options = {:host => nil, :port => nil, :path_prefix => nil})
62
+ # If no options are given the default MusicBrainz webservice will be used.
63
+ # User authentication with username and password is only needed for some
64
+ # services. If you want to query an alternative webservice you can do so
65
+ # by setting the appropriate options.
66
+ #
67
+ # Available options:
68
+ # [:host] Host, defaults to 'musicbrainz.org'.
69
+ # [:port] Port, defaults to 80.
70
+ # [:path_prefix] The path prefix under which the webservice is located on
71
+ # the server. Defaults to '/ws'.
72
+ # [:username] The username to authenticate with.
73
+ # [:password] The password to authenticate with.
74
+ # [:user_agent] Value sent in the User-Agent HTTP header. Defaults to 'rbrainz/0.2'
75
+ def initialize(options={ :host=>nil, :port=>nil, :path_prefix=>'/ws', :username=>nil, :password=>nil, :user_agent=>'rbrainz/0.2' })
76
+ Utils.check_options options, :host, :port, :path_prefix, :username, :password, :user_agent
38
77
  @host = options[:host] ? options[:host] : 'musicbrainz.org'
39
78
  @port = options[:port] ? options[:port] : 80
40
79
  @path_prefix = options[:path_prefix] ? options[:path_prefix] : '/ws'
80
+ @username = options[:username]
81
+ @password = options[:password]
82
+ @user_agent = options[:user_agent] ? options[:user_agent] : 'rbrainz/0.2'
41
83
  @open_timeout = nil
42
84
  @read_timeout = nil
43
85
  end
44
86
 
45
87
  # Query the Webservice with HTTP GET.
46
88
  #
47
- # Raises: +RequestError+, +ResourceNotFoundError+, +AuthenticationError+,
48
- # +ConnectionError+
49
- def get(entity, id, options = {:include => nil, :filter => nil, :version => 1})
50
- url = URI.parse(create_uri(entity, id, options))
89
+ # Returns an IO object on success.
90
+ #
91
+ # Options:
92
+ # [:id] A MBID if querying for a single ressource.
93
+ # [:include] An include object (see AbstractIncludes).
94
+ # [:filter] A filter object (see AbstractFilter).
95
+ # [:version] The version of the webservice to use. Defaults to 1.
96
+ #
97
+ # Raises:: RequestError, ResourceNotFoundError, AuthenticationError,
98
+ # ConnectionError
99
+ # See:: IWebservice#get
100
+ def get(entity_type, options={ :id=>nil, :include=>nil, :filter=>nil, :version=>1 })
101
+ Utils.check_options options, :id, :include, :filter, :version
102
+ url = URI.parse(create_uri(entity_type, options))
51
103
  request = Net::HTTP::Get.new(url.request_uri)
104
+ request['User-Agent'] = @user_agent
52
105
  connection = Net::HTTP.new(url.host, url.port)
53
106
 
54
107
  # Set timeouts
@@ -57,9 +110,16 @@ module MusicBrainz
57
110
 
58
111
  # Make the request
59
112
  begin
60
- response = connection.start {|http|
61
- http.request(request)
62
- }
113
+ response = connection.start do |http|
114
+ response = http.request(request)
115
+ if response.is_a?(Net::HTTPUnauthorized) && @username && @password
116
+ request = Net::HTTP::Get.new(url.request_uri)
117
+ request['User-Agent'] = @user_agent
118
+ request.digest_auth @username, @password, response
119
+ response = http.request(request)
120
+ end
121
+ response
122
+ end
63
123
  rescue Timeout::Error, Errno::ETIMEDOUT
64
124
  raise ConnectionError.new('%s timed out' % url.to_s)
65
125
  end
@@ -71,23 +131,100 @@ module MusicBrainz
71
131
  raise AuthenticationError.new(url.to_s)
72
132
  elsif response.is_a? Net::HTTPNotFound # 404
73
133
  raise ResourceNotFoundError.new(url.to_s)
134
+ elsif response.is_a? Net::HTTPForbidden
135
+ raise AuthenticationError.new(url.to_s)
74
136
  elsif not response.is_a? Net::HTTPSuccess
75
137
  raise ConnectionError.new(response.class.name)
76
138
  end
77
139
 
78
- return response.body
140
+ return ::StringIO.new(response.body)
141
+ end
142
+
143
+ # Send data to the web service via HTTP-POST.
144
+ #
145
+ # Note that this may require authentication. You can set
146
+ # user name, password and realm in the constructor.
147
+ #
148
+ # Returns an IO object on success.
149
+ #
150
+ # Options:
151
+ # [:id] A MBID if querying for a single ressource.
152
+ # [:querystring] A string containing the data to post.
153
+ # [:version] The version of the webservice to use. Defaults to 1.
154
+ #
155
+ # Raises:: ConnectionError, RequestError, AuthenticationError,
156
+ # ResourceNotFoundError
157
+ #
158
+ # See:: IWebservice#post
159
+ def post(entity_type, options={:id=>nil, :querystring=>[], :version=>1})
160
+ Utils.check_options options, :id, :querystring, :version
161
+ url = URI.parse(create_uri(entity_type, options))
162
+ request = Net::HTTP::Post.new(url.request_uri)
163
+ request['User-Agent'] = @user_agent
164
+ request.set_form_data(options[:querystring])
165
+ connection = Net::HTTP.new(url.host, url.port)
166
+
167
+ # Set timeouts
168
+ connection.open_timeout = @open_timeout if @open_timeout
169
+ connection.read_timeout = @read_timeout if @read_timeout
170
+
171
+ # Make the request
172
+ begin
173
+ response = connection.start do |http|
174
+ response = http.request(request)
175
+
176
+ if response.is_a?(Net::HTTPUnauthorized) && @username && @password
177
+ request = Net::HTTP::Post.new(url.request_uri)
178
+ request['User-Agent'] = @user_agent
179
+ request.digest_auth @username, @password, response
180
+ request.set_form_data(options[:querystring])
181
+ response = http.request(request)
182
+ end
183
+ response
184
+ end
185
+ rescue Timeout::Error, Errno::ETIMEDOUT
186
+ raise ConnectionError.new('%s timed out' % url.to_s)
187
+ end
188
+
189
+ # Handle response errors.
190
+ if response.is_a? Net::HTTPBadRequest # 400
191
+ raise RequestError.new(url.to_s)
192
+ elsif response.is_a? Net::HTTPUnauthorized # 401
193
+ raise AuthenticationError.new(url.to_s)
194
+ elsif response.is_a? Net::HTTPNotFound # 404
195
+ raise ResourceNotFoundError.new(url.to_s)
196
+ elsif response.is_a? Net::HTTPForbidden
197
+ raise AuthenticationError.new(url.to_s)
198
+ elsif not response.is_a? Net::HTTPSuccess
199
+ raise ConnectionError.new(response.class.name)
200
+ end
201
+
202
+ return ::StringIO.new(response.body)
79
203
  end
80
204
 
81
205
  private # ----------------------------------------------------------------
82
206
 
83
207
  # Builds a request URI for querying the webservice.
84
- def create_uri(entity, id, options = {:include => nil, :filter => nil, :version => 1})
208
+ def create_uri(entity_type, options = {:id => nil, :include => nil, :filter => nil, :version => 1})
85
209
  # Make sure the version is set
86
210
  options[:version] = 1 if options[:version].nil?
87
211
 
88
212
  # Build the URI
89
- uri = 'http://%s:%d%s/%d/%s/%s?type=%s' %
90
- [@host, @port, @path_prefix, options[:version], entity, id.uuid, 'xml']
213
+ if options[:id]
214
+ # Make sure the id is a MBID object
215
+ id = options[:id]
216
+ unless id.is_a? Model::MBID
217
+ id = Model::MBID.parse(id, entity_type)
218
+ end
219
+
220
+ uri = 'http://%s:%d%s/%d/%s/%s?type=%s' %
221
+ [@host, @port, @path_prefix, options[:version],
222
+ entity_type, id.uuid, 'xml']
223
+ else
224
+ uri = 'http://%s:%d%s/%d/%s/?type=%s' %
225
+ [@host, @port, @path_prefix, options[:version],
226
+ entity_type, 'xml']
227
+ end
91
228
  uri += '&' + options[:include].to_s unless options[:include].nil?
92
229
  uri += '&' + options[:filter].to_s unless options[:filter].nil?
93
230
  return uri
@@ -96,4 +233,4 @@ module MusicBrainz
96
233
  end
97
234
 
98
235
  end
99
- end
236
+ end