rbrainz 0.1.1 → 0.2.0

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