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.
- data/CHANGES +31 -0
- data/LICENSE +1 -1
- data/README +3 -2
- data/Rakefile +40 -22
- data/TODO +6 -23
- data/doc/README.rdoc +50 -21
- data/examples/getartist.rb +6 -4
- data/examples/getuser.rb +30 -0
- data/examples/searchartists.rb +35 -0
- data/lib/rbrainz.rb +12 -7
- data/lib/rbrainz/core_ext.rb +8 -0
- data/lib/rbrainz/core_ext/mbid.rb +30 -0
- data/lib/rbrainz/core_ext/net_http_digest.rb +52 -0
- data/lib/rbrainz/core_ext/range.rb +28 -0
- data/lib/rbrainz/core_ext/range/equality.rb +232 -0
- data/lib/rbrainz/data/countrynames.rb +7 -5
- data/lib/rbrainz/data/languagenames.rb +8 -5
- data/lib/rbrainz/data/releasetypenames.rb +34 -0
- data/lib/rbrainz/data/scriptnames.rb +8 -5
- data/lib/rbrainz/model.rb +27 -35
- data/lib/rbrainz/model/alias.rb +31 -7
- data/lib/rbrainz/model/artist.rb +30 -41
- data/lib/rbrainz/model/collection.rb +102 -0
- data/lib/rbrainz/model/default_factory.rb +78 -0
- data/lib/rbrainz/model/disc.rb +45 -8
- data/lib/rbrainz/model/entity.rb +122 -53
- data/lib/rbrainz/model/incomplete_date.rb +31 -47
- data/lib/rbrainz/model/individual.rb +103 -0
- data/lib/rbrainz/model/label.rb +42 -33
- data/lib/rbrainz/model/mbid.rb +111 -40
- data/lib/rbrainz/model/relation.rb +78 -14
- data/lib/rbrainz/model/release.rb +119 -31
- data/lib/rbrainz/model/release_event.rb +38 -9
- data/lib/rbrainz/model/scored_collection.rb +99 -0
- data/lib/rbrainz/model/tag.rb +39 -0
- data/lib/rbrainz/model/track.rb +37 -13
- data/lib/rbrainz/model/user.rb +48 -0
- data/lib/rbrainz/utils.rb +9 -0
- data/lib/rbrainz/utils/data.rb +78 -0
- data/lib/rbrainz/utils/helper.rb +22 -0
- data/lib/rbrainz/version.rb +15 -0
- data/lib/rbrainz/webservice.rb +32 -6
- data/lib/rbrainz/webservice/filter.rb +124 -47
- data/lib/rbrainz/webservice/includes.rb +49 -10
- data/lib/rbrainz/webservice/mbxml.rb +228 -173
- data/lib/rbrainz/webservice/query.rb +312 -25
- data/lib/rbrainz/webservice/webservice.rb +164 -27
- data/test/lib/mock_webservice.rb +53 -0
- data/test/lib/test_entity.rb +27 -8
- data/test/lib/test_factory.rb +47 -0
- data/test/lib/testing_helper.rb +7 -5
- data/test/test-data/invalid/artist/tags_1.xml +6 -0
- data/test/test-data/valid/artist/Tchaikovsky-2.xml +12 -0
- data/test/test-data/valid/label/Atlantic_Records_2.xml +3 -0
- data/test/test-data/valid/label/Atlantic_Records_3.xml +11 -0
- data/test/test-data/valid/release/Highway_61_Revisited_2.xml +12 -0
- data/test/test-data/valid/track/Silent_All_These_Years_6.xml +8 -0
- data/test/test_alias.rb +13 -7
- data/test/test_artist.rb +26 -4
- data/test/test_artist_filter.rb +11 -6
- data/test/test_artist_includes.rb +11 -6
- data/test/test_collection.rb +66 -0
- data/test/test_default_factory.rb +75 -0
- data/test/test_disc.rb +9 -4
- data/test/test_incomplete_date.rb +21 -14
- data/test/test_label.rb +56 -18
- data/test/test_label_filter.rb +10 -5
- data/test/test_label_includes.rb +11 -6
- data/test/test_mbid.rb +34 -19
- data/test/test_mbxml.rb +242 -72
- data/test/test_query.rb +92 -7
- data/test/test_range_equality.rb +144 -0
- data/test/test_relation.rb +18 -7
- data/test/test_release.rb +15 -4
- data/test/test_release_event.rb +16 -4
- data/test/test_release_filter.rb +11 -5
- data/test/test_release_includes.rb +11 -6
- data/test/test_scored_collection.rb +86 -0
- data/test/test_tag.rb +39 -0
- data/test/test_track.rb +15 -4
- data/test/test_track_filter.rb +11 -5
- data/test/test_track_includes.rb +11 -6
- data/test/test_utils.rb +41 -0
- data/test/test_webservice.rb +16 -17
- metadata +93 -57
@@ -1,7 +1,9 @@
|
|
1
|
-
# $Id: query.rb
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
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
|
-
|
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
|
-
|
19
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
28
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
37
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
46
|
-
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
2
|
-
#
|
3
|
-
#
|
4
|
-
#
|
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
|
16
|
-
#
|
17
|
-
|
18
|
-
|
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
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
|
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
|
-
#
|
31
|
-
#
|
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
|
-
|
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
|
-
#
|
48
|
-
#
|
49
|
-
|
50
|
-
|
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
|
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(
|
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
|
-
|
90
|
-
|
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
|