1 """Classes for interacting with the MusicBrainz XML web service.
2
3 The L{WebService} class talks to a server implementing the MusicBrainz XML
4 web service. It mainly handles URL generation and network I/O. Use this
5 if maximum control is needed.
6
7 The L{Query} class provides a convenient interface to the most commonly
8 used features of the web service. By default it uses L{WebService} to
9 retrieve data and the L{XML parser <musicbrainz2.wsxml>} to parse the
10 responses. The results are object trees using the L{MusicBrainz domain
11 model <musicbrainz2.model>}.
12
13 @author: Matthias Friedrich <matt@mafr.de>
14 """
15 __revision__ = '$Id: webservice.py 13325 2011-11-03 13:39:40Z luks $'
16
17 import urllib
18 import urllib2
19 import urlparse
20 import logging
21 import musicbrainz2
22 from musicbrainz2.model import Release
23 from musicbrainz2.wsxml import MbXmlParser, ParseError
24 import musicbrainz2.utils as mbutils
25
26 __all__ = [
27 'WebServiceError', 'AuthenticationError', 'ConnectionError',
28 'RequestError', 'ResourceNotFoundError', 'ResponseError',
29 'IIncludes', 'ArtistIncludes', 'ReleaseIncludes', 'TrackIncludes',
30 'LabelIncludes', 'ReleaseGroupIncludes',
31 'IFilter', 'ArtistFilter', 'ReleaseFilter', 'TrackFilter',
32 'UserFilter', 'LabelFilter', 'ReleaseGroupFilter',
33 'IWebService', 'WebService', 'Query',
34 ]
35
36
38 """An interface all concrete web service classes have to implement.
39
40 All web service classes have to implement this and follow the
41 method specifications.
42 """
43
44 - def get(self, entity, id_, include, filter, version):
45 """Query the web service.
46
47 Using this method, you can either get a resource by id (using
48 the C{id_} parameter, or perform a query on all resources of
49 a type.
50
51 The C{filter} and the C{id_} parameter exclude each other. If
52 you are using a filter, you may not set C{id_} and vice versa.
53
54 Returns a file-like object containing the result or raises a
55 L{WebServiceError} or one of its subclasses in case of an
56 error. Which one is used depends on the implementing class.
57
58 @param entity: a string containing the entity's name
59 @param id_: a string containing a UUID, or the empty string
60 @param include: a tuple containing values for the 'inc' parameter
61 @param filter: parameters, depending on the entity
62 @param version: a string containing the web service version to use
63
64 @return: a file-like object
65
66 @raise WebServiceError: in case of errors
67 """
68 raise NotImplementedError()
69
70
71 - def post(self, entity, id_, data, version):
72 """Submit data to the web service.
73
74 @param entity: a string containing the entity's name
75 @param id_: a string containing a UUID, or the empty string
76 @param data: A string containing the data to post
77 @param version: a string containing the web service version to use
78
79 @return: a file-like object
80
81 @raise WebServiceError: in case of errors
82 """
83 raise NotImplementedError()
84
85
87 """A web service error has occurred.
88
89 This is the base class for several other web service related
90 exceptions.
91 """
92
93 - def __init__(self, msg='Webservice Error', reason=None):
94 """Constructor.
95
96 Set C{msg} to an error message which explains why this
97 exception was raised. The C{reason} parameter should be the
98 original exception which caused this L{WebService} exception
99 to be raised. If given, it has to be an instance of
100 C{Exception} or one of its child classes.
101
102 @param msg: a string containing an error message
103 @param reason: another exception instance, or None
104 """
105 Exception.__init__(self)
106 self.msg = msg
107 self.reason = reason
108
110 """Makes this class printable.
111
112 @return: a string containing an error message
113 """
114 return self.msg
115
116
118 """Getting a server connection failed.
119
120 This exception is mostly used if the client couldn't connect to
121 the server because of an invalid host name or port. It doesn't
122 make sense if the web service in question doesn't use the network.
123 """
124 pass
125
126
128 """An invalid request was made.
129
130 This exception is raised if the client made an invalid request.
131 That could be syntactically invalid identifiers or unknown or
132 invalid parameter values.
133 """
134 pass
135
136
138 """No resource with the given ID exists.
139
140 This is usually a wrapper around IOError (which is superclass of
141 HTTPError).
142 """
143 pass
144
145
147 """Authentication failed.
148
149 This is thrown if user name, password or realm were invalid while
150 trying to access a protected resource.
151 """
152 pass
153
154
156 """The returned resource was invalid.
157
158 This may be due to a malformed XML document or if the requested
159 data wasn't part of the response. It can only occur in case of
160 bugs in the web service itself.
161 """
162 pass
163
165 """Patched DigestAuthHandler to correctly handle Digest Auth according to RFC 2617.
166
167 This will allow multiple qop values in the WWW-Authenticate header (e.g. "auth,auth-int").
168 The only supported qop value is still auth, though.
169 See http://bugs.python.org/issue9714
170
171 @author: Kuno Woudt
172 """
174 qop = chal.get('qop')
175 if qop and ',' in qop and 'auth' in qop.split(','):
176 chal['qop'] = 'auth'
177
178 return urllib2.HTTPDigestAuthHandler.get_authorization(self, req, chal)
179
181 """An interface to the MusicBrainz XML web service via HTTP.
182
183 By default, this class uses the MusicBrainz server but may be
184 configured for accessing other servers as well using the
185 L{constructor <__init__>}. This implements L{IWebService}, so
186 additional documentation on method parameters can be found there.
187 """
188
189 - def __init__(self, host='musicbrainz.org', port=80, pathPrefix='/ws',
190 username=None, password=None, realm='musicbrainz.org',
191 opener=None, userAgent=None):
192 """Constructor.
193
194 This can be used without parameters. In this case, the
195 MusicBrainz server will be used.
196
197 @param host: a string containing a host name
198 @param port: an integer containing a port number
199 @param pathPrefix: a string prepended to all URLs
200 @param username: a string containing a MusicBrainz user name
201 @param password: a string containing the user's password
202 @param realm: a string containing the realm used for authentication
203 @param opener: an C{urllib2.OpenerDirector} object used for queries
204 @param userAgent: a string containing the user agent
205 """
206 self._host = host
207 self._port = port
208 self._username = username
209 self._password = password
210 self._realm = realm
211 self._pathPrefix = pathPrefix
212 self._log = logging.getLogger(str(self.__class__))
213
214 if opener is None:
215 self._opener = urllib2.build_opener()
216 else:
217 self._opener = opener
218
219 if userAgent is None:
220 self._userAgent = "python-musicbrainz/" + musicbrainz2.__version__
221 else:
222 self._userAgent = userAgent.replace("-", "/") \
223 + " python-musicbrainz/" \
224 + musicbrainz2.__version__
225
226 passwordMgr = self._RedirectPasswordMgr()
227 authHandler = DigestAuthHandler(passwordMgr)
228 authHandler.add_password(self._realm, (),
229 self._username, self._password)
230 self._opener.add_handler(authHandler)
231
232
233 - def _makeUrl(self, entity, id_, include=( ), filter={ },
234 version='1', type_='xml'):
235 params = dict(filter)
236 if type_ is not None:
237 params['type'] = type_
238 if len(include) > 0:
239 params['inc'] = ' '.join(include)
240
241 netloc = self._host
242 if self._port != 80:
243 netloc += ':' + str(self._port)
244 path = '/'.join((self._pathPrefix, version, entity, id_))
245
246 query = urllib.urlencode(params)
247
248 url = urlparse.urlunparse(('http', netloc, path, '', query,''))
249
250 return url
251
252
254 req = urllib2.Request(url)
255 req.add_header('User-Agent', self._userAgent)
256 return self._opener.open(req, data)
257
258
259 - def get(self, entity, id_, include=( ), filter={ }, version='1'):
260 """Query the web service via HTTP-GET.
261
262 Returns a file-like object containing the result or raises a
263 L{WebServiceError}. Conditions leading to errors may be
264 invalid entities, IDs, C{include} or C{filter} parameters
265 and unsupported version numbers.
266
267 @raise ConnectionError: couldn't connect to server
268 @raise RequestError: invalid IDs or parameters
269 @raise AuthenticationError: invalid user name and/or password
270 @raise ResourceNotFoundError: resource doesn't exist
271
272 @see: L{IWebService.get}
273 """
274 url = self._makeUrl(entity, id_, include, filter, version)
275
276 self._log.debug('GET ' + url)
277
278 try:
279 return self._openUrl(url)
280 except urllib2.HTTPError, e:
281 self._log.debug("GET failed: " + str(e))
282 if e.code == 400:
283 raise RequestError(str(e), e)
284 elif e.code == 401:
285 raise AuthenticationError(str(e), e)
286 elif e.code == 404:
287 raise ResourceNotFoundError(str(e), e)
288 else:
289 raise WebServiceError(str(e), e)
290 except urllib2.URLError, e:
291 self._log.debug("GET failed: " + str(e))
292 raise ConnectionError(str(e), e)
293
294
295 - def post(self, entity, id_, data, version='1'):
296 """Send data to the web service via HTTP-POST.
297
298 Note that this may require authentication. You can set
299 user name, password and realm in the L{constructor <__init__>}.
300
301 @raise ConnectionError: couldn't connect to server
302 @raise RequestError: invalid IDs or parameters
303 @raise AuthenticationError: invalid user name and/or password
304 @raise ResourceNotFoundError: resource doesn't exist
305
306 @see: L{IWebService.post}
307 """
308 url = self._makeUrl(entity, id_, version=version, type_=None)
309
310 self._log.debug('POST ' + url)
311 self._log.debug('POST-BODY: ' + data)
312
313 try:
314 return self._openUrl(url, data)
315 except urllib2.HTTPError, e:
316 self._log.debug("POST failed: " + str(e))
317 if e.code == 400:
318 raise RequestError(str(e), e)
319 elif e.code == 401:
320 raise AuthenticationError(str(e), e)
321 elif e.code == 404:
322 raise ResourceNotFoundError(str(e), e)
323 else:
324 raise WebServiceError(str(e), e)
325 except urllib2.URLError, e:
326 self._log.debug("POST failed: " + str(e))
327 raise ConnectionError(str(e), e)
328
329
330
331
332
333
337
339
340 try:
341 return self._realms[realm]
342 except KeyError:
343 return (None, None)
344
346
347 self._realms[realm] = (username, password)
348
349
351 """A filter for collections.
352
353 This is the interface all filters have to implement. Filter classes
354 are initialized with a set of criteria and are then applied to
355 collections of items. The criteria are usually strings or integer
356 values, depending on the filter.
357
358 Note that all strings passed to filters should be unicode strings
359 (python type C{unicode}). Standard strings are converted to unicode
360 internally, but have a limitation: Only 7 Bit pure ASCII characters
361 may be used, otherwise a C{UnicodeDecodeError} is raised.
362 """
364 """Create a list of query parameters.
365
366 This method creates a list of (C{parameter}, C{value}) tuples,
367 based on the contents of the implementing subclass.
368 C{parameter} is a string containing a parameter name
369 and C{value} an arbitrary string. No escaping of those strings
370 is required.
371
372 @return: a sequence of (key, value) pairs
373 """
374 raise NotImplementedError()
375
376
378 """A filter for the artist collection."""
379
380 - def __init__(self, name=None, limit=None, offset=None, query=None):
381 """Constructor.
382
383 The C{query} parameter may contain a query in U{Lucene syntax
384 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
385 Note that the C{name} and C{query} may not be used together.
386
387 @param name: a unicode string containing the artist's name
388 @param limit: the maximum number of artists to return
389 @param offset: start results at this zero-based offset
390 @param query: a string containing a query in Lucene syntax
391 """
392 self._params = [
393 ('name', name),
394 ('limit', limit),
395 ('offset', offset),
396 ('query', query),
397 ]
398
399 if not _paramsValid(self._params):
400 raise ValueError('invalid combination of parameters')
401
403 return _createParameters(self._params)
404
405
407 """A filter for the label collection."""
408
409 - def __init__(self, name=None, limit=None, offset=None, query=None):
410 """Constructor.
411
412 The C{query} parameter may contain a query in U{Lucene syntax
413 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
414 Note that the C{name} and C{query} may not be used together.
415
416 @param name: a unicode string containing the label's name
417 @param limit: the maximum number of labels to return
418 @param offset: start results at this zero-based offset
419 @param query: a string containing a query in Lucene syntax
420 """
421 self._params = [
422 ('name', name),
423 ('limit', limit),
424 ('offset', offset),
425 ('query', query),
426 ]
427
428 if not _paramsValid(self._params):
429 raise ValueError('invalid combination of parameters')
430
432 return _createParameters(self._params)
433
435 """A filter for the release group collection."""
436
437 - def __init__(self, title=None, releaseTypes=None, artistName=None,
438 artistId=None, limit=None, offset=None, query=None):
439 """Constructor.
440
441 If C{artistId} is set, only releases matching those IDs are
442 returned. The C{releaseTypes} parameter allows you to limit
443 the types of the release groups returned. You can set it to
444 C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
445 to only get officially released albums. Note that those values
446 are connected using the I{AND} operator. MusicBrainz' support
447 is currently very limited, so C{Release.TYPE_LIVE} and
448 C{Release.TYPE_COMPILATION} exclude each other (see U{the
449 documentation on release attributes
450 <http://wiki.musicbrainz.org/AlbumAttribute>} for more
451 information and all valid values).
452
453 If both the C{artistName} and the C{artistId} parameter are
454 given, the server will ignore C{artistName}.
455
456 The C{query} parameter may contain a query in U{Lucene syntax
457 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
458 Note that C{query} may not be used together with the other
459 parameters except for C{limit} and C{offset}.
460
461 @param title: a unicode string containing the release group's title
462 @param releaseTypes: a sequence of release type URIs
463 @param artistName: a unicode string containing the artist's name
464 @param artistId: a unicode string containing the artist's ID
465 @param limit: the maximum number of release groups to return
466 @param offset: start results at this zero-based offset
467 @param query: a string containing a query in Lucene syntax
468
469 @see: the constants in L{musicbrainz2.model.Release}
470 """
471 if releaseTypes is None or len(releaseTypes) == 0:
472 releaseTypesStr = None
473 else:
474 releaseTypesStr = ' '.join(map(mbutils.extractFragment, releaseTypes))
475
476 self._params = [
477 ('title', title),
478 ('releasetypes', releaseTypesStr),
479 ('artist', artistName),
480 ('artistid', mbutils.extractUuid(artistId)),
481 ('limit', limit),
482 ('offset', offset),
483 ('query', query),
484 ]
485
486 if not _paramsValid(self._params):
487 raise ValueError('invalid combination of parameters')
488
490 return _createParameters(self._params)
491
492
494 """A filter for the release collection."""
495
496 - def __init__(self, title=None, discId=None, releaseTypes=None,
497 artistName=None, artistId=None, limit=None,
498 offset=None, query=None, trackCount=None):
499 """Constructor.
500
501 If C{discId} or C{artistId} are set, only releases matching
502 those IDs are returned. The C{releaseTypes} parameter allows
503 to limit the types of the releases returned. You can set it to
504 C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example,
505 to only get officially released albums. Note that those values
506 are connected using the I{AND} operator. MusicBrainz' support
507 is currently very limited, so C{Release.TYPE_LIVE} and
508 C{Release.TYPE_COMPILATION} exclude each other (see U{the
509 documentation on release attributes
510 <http://wiki.musicbrainz.org/AlbumAttribute>} for more
511 information and all valid values).
512
513 If both the C{artistName} and the C{artistId} parameter are
514 given, the server will ignore C{artistName}.
515
516 The C{query} parameter may contain a query in U{Lucene syntax
517 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
518 Note that C{query} may not be used together with the other
519 parameters except for C{limit} and C{offset}.
520
521 @param title: a unicode string containing the release's title
522 @param discId: a unicode string containing the DiscID
523 @param releaseTypes: a sequence of release type URIs
524 @param artistName: a unicode string containing the artist's name
525 @param artistId: a unicode string containing the artist's ID
526 @param limit: the maximum number of releases to return
527 @param offset: start results at this zero-based offset
528 @param query: a string containing a query in Lucene syntax
529 @param trackCount: the number of tracks in the release
530
531 @see: the constants in L{musicbrainz2.model.Release}
532 """
533 if releaseTypes is None or len(releaseTypes) == 0:
534 releaseTypesStr = None
535 else:
536 tmp = [ mbutils.extractFragment(x) for x in releaseTypes ]
537 releaseTypesStr = ' '.join(tmp)
538
539 self._params = [
540 ('title', title),
541 ('discid', discId),
542 ('releasetypes', releaseTypesStr),
543 ('artist', artistName),
544 ('artistid', mbutils.extractUuid(artistId)),
545 ('limit', limit),
546 ('offset', offset),
547 ('query', query),
548 ('count', trackCount),
549 ]
550
551 if not _paramsValid(self._params):
552 raise ValueError('invalid combination of parameters')
553
555 return _createParameters(self._params)
556
557
559 """A filter for the track collection."""
560
561 - def __init__(self, title=None, artistName=None, artistId=None,
562 releaseTitle=None, releaseId=None,
563 duration=None, puid=None, limit=None, offset=None,
564 query=None):
565 """Constructor.
566
567 If C{artistId}, C{releaseId} or C{puid} are set, only tracks
568 matching those IDs are returned.
569
570 The server will ignore C{artistName} and C{releaseTitle} if
571 C{artistId} or ${releaseId} are set respectively.
572
573 The C{query} parameter may contain a query in U{Lucene syntax
574 <http://lucene.apache.org/java/docs/queryparsersyntax.html>}.
575 Note that C{query} may not be used together with the other
576 parameters except for C{limit} and C{offset}.
577
578 @param title: a unicode string containing the track's title
579 @param artistName: a unicode string containing the artist's name
580 @param artistId: a string containing the artist's ID
581 @param releaseTitle: a unicode string containing the release's title
582 @param releaseId: a string containing the release's title
583 @param duration: the track's length in milliseconds
584 @param puid: a string containing a PUID
585 @param limit: the maximum number of releases to return
586 @param offset: start results at this zero-based offset
587 @param query: a string containing a query in Lucene syntax
588 """
589 self._params = [
590 ('title', title),
591 ('artist', artistName),
592 ('artistid', mbutils.extractUuid(artistId)),
593 ('release', releaseTitle),
594 ('releaseid', mbutils.extractUuid(releaseId)),
595 ('duration', duration),
596 ('puid', puid),
597 ('limit', limit),
598 ('offset', offset),
599 ('query', query),
600 ]
601
602 if not _paramsValid(self._params):
603 raise ValueError('invalid combination of parameters')
604
606 return _createParameters(self._params)
607
608
610 """A filter for the user collection."""
611
613 """Constructor.
614
615 @param name: a unicode string containing a MusicBrainz user name
616 """
617 self._name = name
618
620 if self._name is not None:
621 return [ ('name', self._name.encode('utf-8')) ]
622 else:
623 return [ ]
624
625
627 """An interface implemented by include tag generators."""
630
631
633 """A specification on how much data to return with an artist.
634
635 Example:
636
637 >>> from musicbrainz2.model import Release
638 >>> from musicbrainz2.webservice import ArtistIncludes
639 >>> inc = ArtistIncludes(artistRelations=True, releaseRelations=True,
640 ... releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL))
641 >>>
642
643 The MusicBrainz server only supports some combinations of release
644 types for the C{releases} and C{vaReleases} include tags. At the
645 moment, not more than two release types should be selected, while
646 one of them has to be C{Release.TYPE_OFFICIAL},
647 C{Release.TYPE_PROMOTION} or C{Release.TYPE_BOOTLEG}.
648
649 @note: Only one of C{releases} and C{vaReleases} may be given.
650 """
651 - def __init__(self, aliases=False, releases=(), vaReleases=(),
652 artistRelations=False, releaseRelations=False,
653 trackRelations=False, urlRelations=False, tags=False,
654 ratings=False, releaseGroups=False):
655
656 assert not isinstance(releases, basestring)
657 assert not isinstance(vaReleases, basestring)
658 assert len(releases) == 0 or len(vaReleases) == 0
659
660 self._includes = {
661 'aliases': aliases,
662 'artist-rels': artistRelations,
663 'release-groups': releaseGroups,
664 'release-rels': releaseRelations,
665 'track-rels': trackRelations,
666 'url-rels': urlRelations,
667 'tags': tags,
668 'ratings': ratings,
669 }
670
671 for elem in releases:
672 self._includes['sa-' + mbutils.extractFragment(elem)] = True
673
674 for elem in vaReleases:
675 self._includes['va-' + mbutils.extractFragment(elem)] = True
676
679
680
682 """A specification on how much data to return with a release."""
683 - def __init__(self, artist=False, counts=False, releaseEvents=False,
684 discs=False, tracks=False,
685 artistRelations=False, releaseRelations=False,
686 trackRelations=False, urlRelations=False,
687 labels=False, tags=False, ratings=False, isrcs=False,
688 releaseGroup=False):
689 self._includes = {
690 'artist': artist,
691 'counts': counts,
692 'labels': labels,
693 'release-groups': releaseGroup,
694 'release-events': releaseEvents,
695 'discs': discs,
696 'tracks': tracks,
697 'artist-rels': artistRelations,
698 'release-rels': releaseRelations,
699 'track-rels': trackRelations,
700 'url-rels': urlRelations,
701 'tags': tags,
702 'ratings': ratings,
703 'isrcs': isrcs,
704 }
705
706
707
708 if labels and not releaseEvents:
709 self._includes['release-events'] = True
710
711 if isrcs and not tracks:
712 self._includes['tracks'] = True
713
716
717
719 """A specification on how much data to return with a release group."""
720
721 - def __init__(self, artist=False, releases=False, tags=False):
722 """Constructor.
723
724 @param artist: Whether to include the release group's main artist info.
725 @param releases: Whether to include the release group's releases.
726 """
727 self._includes = {
728 'artist': artist,
729 'releases': releases,
730 }
731
734
735
737 """A specification on how much data to return with a track."""
738 - def __init__(self, artist=False, releases=False, puids=False,
739 artistRelations=False, releaseRelations=False,
740 trackRelations=False, urlRelations=False, tags=False,
741 ratings=False, isrcs=False):
742 self._includes = {
743 'artist': artist,
744 'releases': releases,
745 'puids': puids,
746 'artist-rels': artistRelations,
747 'release-rels': releaseRelations,
748 'track-rels': trackRelations,
749 'url-rels': urlRelations,
750 'tags': tags,
751 'ratings': ratings,
752 'isrcs': isrcs,
753 }
754
757
758
760 """A specification on how much data to return with a label."""
761 - def __init__(self, aliases=False, tags=False, ratings=False):
762 self._includes = {
763 'aliases': aliases,
764 'tags': tags,
765 'ratings': ratings,
766 }
767
770
771
773 """A simple interface to the MusicBrainz web service.
774
775 This is a facade which provides a simple interface to the MusicBrainz
776 web service. It hides all the details like fetching data from a server,
777 parsing the XML and creating an object tree. Using this class, you can
778 request data by ID or search the I{collection} of all resources
779 (artists, releases, or tracks) to retrieve those matching given
780 criteria. This document contains examples to get you started.
781
782
783 Working with Identifiers
784 ========================
785
786 MusicBrainz uses absolute URIs as identifiers. For example, the artist
787 'Tori Amos' is identified using the following URI::
788 http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5
789
790 In some situations it is obvious from the context what type of
791 resource an ID refers to. In these cases, abbreviated identifiers may
792 be used, which are just the I{UUID} part of the URI. Thus the ID above
793 may also be written like this::
794 c0b2500e-0cef-4130-869d-732b23ed9df5
795
796 All methods in this class which require IDs accept both the absolute
797 URI and the abbreviated form (aka the relative URI).
798
799
800 Creating a Query Object
801 =======================
802
803 In most cases, creating a L{Query} object is as simple as this:
804
805 >>> import musicbrainz2.webservice as ws
806 >>> q = ws.Query()
807 >>>
808
809 The instantiated object uses the standard L{WebService} class to
810 access the MusicBrainz web service. If you want to use a different
811 server or you have to pass user name and password because one of
812 your queries requires authentication, you have to create the
813 L{WebService} object yourself and configure it appropriately.
814 This example uses the MusicBrainz test server and also sets
815 authentication data:
816
817 >>> import musicbrainz2.webservice as ws
818 >>> service = ws.WebService(host='test.musicbrainz.org',
819 ... username='whatever', password='secret')
820 >>> q = ws.Query(service)
821 >>>
822
823
824 Querying for Individual Resources
825 =================================
826
827 If the MusicBrainz ID of a resource is known, then the L{getArtistById},
828 L{getReleaseById}, or L{getTrackById} method can be used to retrieve
829 it. Example:
830
831 >>> import musicbrainz2.webservice as ws
832 >>> q = ws.Query()
833 >>> artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
834 >>> artist.name
835 u'Tori Amos'
836 >>> artist.sortName
837 u'Amos, Tori'
838 >>> print artist.type
839 http://musicbrainz.org/ns/mmd-1.0#Person
840 >>>
841
842 This returned just the basic artist data, however. To get more detail
843 about a resource, the C{include} parameters may be used which expect
844 an L{ArtistIncludes}, L{ReleaseIncludes}, or L{TrackIncludes} object,
845 depending on the resource type.
846
847 To get data about a release which also includes the main artist
848 and all tracks, for example, the following query can be used:
849
850 >>> import musicbrainz2.webservice as ws
851 >>> q = ws.Query()
852 >>> releaseId = '33dbcf02-25b9-4a35-bdb7-729455f33ad7'
853 >>> include = ws.ReleaseIncludes(artist=True, tracks=True)
854 >>> release = q.getReleaseById(releaseId, include)
855 >>> release.title
856 u'Tales of a Librarian'
857 >>> release.artist.name
858 u'Tori Amos'
859 >>> release.tracks[0].title
860 u'Precious Things'
861 >>>
862
863 Note that the query gets more expensive for the server the more
864 data you request, so please be nice.
865
866
867 Searching in Collections
868 ========================
869
870 For each resource type (artist, release, and track), there is one
871 collection which contains all resources of a type. You can search
872 these collections using the L{getArtists}, L{getReleases}, and
873 L{getTracks} methods. The collections are huge, so you have to
874 use filters (L{ArtistFilter}, L{ReleaseFilter}, or L{TrackFilter})
875 to retrieve only resources matching given criteria.
876
877 For example, If you want to search the release collection for
878 releases with a specified DiscID, you would use L{getReleases}
879 and a L{ReleaseFilter} object:
880
881 >>> import musicbrainz2.webservice as ws
882 >>> q = ws.Query()
883 >>> filter = ws.ReleaseFilter(discId='8jJklE258v6GofIqDIrE.c5ejBE-')
884 >>> results = q.getReleases(filter=filter)
885 >>> results[0].score
886 100
887 >>> results[0].release.title
888 u'Under the Pink'
889 >>>
890
891 The query returns a list of results (L{wsxml.ReleaseResult} objects
892 in this case), which are ordered by score, with a higher score
893 indicating a better match. Note that those results don't contain
894 all the data about a resource. If you need more detail, you can then
895 use the L{getArtistById}, L{getReleaseById}, or L{getTrackById}
896 methods to request the resource.
897
898 All filters support the C{limit} argument to limit the number of
899 results returned. This defaults to 25, but the server won't send
900 more than 100 results to save bandwidth and processing power. Using
901 C{limit} and the C{offset} parameter, you can page through the
902 results.
903
904
905 Error Handling
906 ==============
907
908 All methods in this class raise a L{WebServiceError} exception in case
909 of errors. Depending on the method, a subclass of L{WebServiceError} may
910 be raised which allows an application to handle errors more precisely.
911 The following example handles connection errors (invalid host name
912 etc.) separately and all other web service errors in a combined
913 catch clause:
914
915 >>> try:
916 ... artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5')
917 ... except ws.ConnectionError, e:
918 ... pass # implement your error handling here
919 ... except ws.WebServiceError, e:
920 ... pass # catches all other web service errors
921 ...
922 >>>
923 """
924
926 """Constructor.
927
928 The C{ws} parameter has to be a subclass of L{IWebService}.
929 If it isn't given, the C{wsFactory} parameter is used to
930 create an L{IWebService} subclass.
931
932 If the constructor is called without arguments, an instance
933 of L{WebService} is used, preconfigured to use the MusicBrainz
934 server. This should be enough for most users.
935
936 If you want to use queries which require authentication you
937 have to pass a L{WebService} instance where user name and
938 password have been set.
939
940 The C{clientId} parameter is required for data submission.
941 The format is C{'application-version'}, where C{application}
942 is your application's name and C{version} is a version
943 number which may not include a '-' character.
944 Even if you don't plan to submit data, setting this parameter is
945 encouraged because it will set the user agent used to make requests if
946 you don't supply the C{ws} parameter.
947
948 @param ws: a subclass instance of L{IWebService}, or None
949 @param wsFactory: a callable object which creates an object
950 @param clientId: a unicode string containing the application's ID
951 """
952 if ws is None:
953 self._ws = wsFactory(userAgent=clientId)
954 else:
955 self._ws = ws
956
957 self._clientId = clientId
958 self._log = logging.getLogger(str(self.__class__))
959
960
962 """Returns an artist.
963
964 If no artist with that ID can be found, C{include} contains
965 invalid tags or there's a server problem, an exception is
966 raised.
967
968 @param id_: a string containing the artist's ID
969 @param include: an L{ArtistIncludes} object, or None
970
971 @return: an L{Artist <musicbrainz2.model.Artist>} object, or None
972
973 @raise ConnectionError: couldn't connect to server
974 @raise RequestError: invalid ID or include tags
975 @raise ResourceNotFoundError: artist doesn't exist
976 @raise ResponseError: server returned invalid data
977 """
978 uuid = mbutils.extractUuid(id_, 'artist')
979 result = self._getFromWebService('artist', uuid, include)
980 artist = result.getArtist()
981 if artist is not None:
982 return artist
983 else:
984 raise ResponseError("server didn't return artist")
985
986
988 """Returns artists matching given criteria.
989
990 @param filter: an L{ArtistFilter} object
991
992 @return: a list of L{musicbrainz2.wsxml.ArtistResult} objects
993
994 @raise ConnectionError: couldn't connect to server
995 @raise RequestError: invalid ID or include tags
996 @raise ResponseError: server returned invalid data
997 """
998 result = self._getFromWebService('artist', '', filter=filter)
999 return result.getArtistResults()
1000
1002 """Returns a L{model.Label}
1003
1004 If no label with that ID can be found, or there is a server problem,
1005 an exception is raised.
1006
1007 @param id_: a string containing the label's ID.
1008
1009 @raise ConnectionError: couldn't connect to server
1010 @raise RequestError: invalid ID or include tags
1011 @raise ResourceNotFoundError: release doesn't exist
1012 @raise ResponseError: server returned invalid data
1013 """
1014 uuid = mbutils.extractUuid(id_, 'label')
1015 result = self._getFromWebService('label', uuid, include)
1016 label = result.getLabel()
1017 if label is not None:
1018 return label
1019 else:
1020 raise ResponseError("server didn't return a label")
1021
1023 result = self._getFromWebService('label', '', filter=filter)
1024 return result.getLabelResults()
1025
1027 """Returns a release.
1028
1029 If no release with that ID can be found, C{include} contains
1030 invalid tags or there's a server problem, and exception is
1031 raised.
1032
1033 @param id_: a string containing the release's ID
1034 @param include: a L{ReleaseIncludes} object, or None
1035
1036 @return: a L{Release <musicbrainz2.model.Release>} object, or None
1037
1038 @raise ConnectionError: couldn't connect to server
1039 @raise RequestError: invalid ID or include tags
1040 @raise ResourceNotFoundError: release doesn't exist
1041 @raise ResponseError: server returned invalid data
1042 """
1043 uuid = mbutils.extractUuid(id_, 'release')
1044 result = self._getFromWebService('release', uuid, include)
1045 release = result.getRelease()
1046 if release is not None:
1047 return release
1048 else:
1049 raise ResponseError("server didn't return release")
1050
1051
1053 """Returns releases matching given criteria.
1054
1055 @param filter: a L{ReleaseFilter} object
1056
1057 @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
1058
1059 @raise ConnectionError: couldn't connect to server
1060 @raise RequestError: invalid ID or include tags
1061 @raise ResponseError: server returned invalid data
1062 """
1063 result = self._getFromWebService('release', '', filter=filter)
1064 return result.getReleaseResults()
1065
1067 """Returns a release group.
1068
1069 If no release group with that ID can be found, C{include}
1070 contains invalid tags, or there's a server problem, an
1071 exception is raised.
1072
1073 @param id_: a string containing the release group's ID
1074 @param include: a L{ReleaseGroupIncludes} object, or None
1075
1076 @return: a L{ReleaseGroup <musicbrainz2.model.ReleaseGroup>} object, or None
1077
1078 @raise ConnectionError: couldn't connect to server
1079 @raise RequestError: invalid ID or include tags
1080 @raise ResourceNotFoundError: release doesn't exist
1081 @raise ResponseError: server returned invalid data
1082 """
1083 uuid = mbutils.extractUuid(id_, 'release-group')
1084 result = self._getFromWebService('release-group', uuid, include)
1085 releaseGroup = result.getReleaseGroup()
1086 if releaseGroup is not None:
1087 return releaseGroup
1088 else:
1089 raise ResponseError("server didn't return releaseGroup")
1090
1092 """Returns release groups matching the given criteria.
1093
1094 @param filter: a L{ReleaseGroupFilter} object
1095
1096 @return: a list of L{musicbrainz2.wsxml.ReleaseGroupResult} objects
1097
1098 @raise ConnectionError: couldn't connect to server
1099 @raise RequestError: invalid ID or include tags
1100 @raise ResponseError: server returned invalid data
1101 """
1102 result = self._getFromWebService('release-group', '', filter=filter)
1103 return result.getReleaseGroupResults()
1104
1106 """Returns a track.
1107
1108 If no track with that ID can be found, C{include} contains
1109 invalid tags or there's a server problem, an exception is
1110 raised.
1111
1112 @param id_: a string containing the track's ID
1113 @param include: a L{TrackIncludes} object, or None
1114
1115 @return: a L{Track <musicbrainz2.model.Track>} object, or None
1116
1117 @raise ConnectionError: couldn't connect to server
1118 @raise RequestError: invalid ID or include tags
1119 @raise ResourceNotFoundError: track doesn't exist
1120 @raise ResponseError: server returned invalid data
1121 """
1122 uuid = mbutils.extractUuid(id_, 'track')
1123 result = self._getFromWebService('track', uuid, include)
1124 track = result.getTrack()
1125 if track is not None:
1126 return track
1127 else:
1128 raise ResponseError("server didn't return track")
1129
1130
1132 """Returns tracks matching given criteria.
1133
1134 @param filter: a L{TrackFilter} object
1135
1136 @return: a list of L{musicbrainz2.wsxml.TrackResult} objects
1137
1138 @raise ConnectionError: couldn't connect to server
1139 @raise RequestError: invalid ID or include tags
1140 @raise ResponseError: server returned invalid data
1141 """
1142 result = self._getFromWebService('track', '', filter=filter)
1143 return result.getTrackResults()
1144
1145
1147 """Returns information about a MusicBrainz user.
1148
1149 You can only request user data if you know the user name and
1150 password for that account. If username and/or password are
1151 incorrect, an L{AuthenticationError} is raised.
1152
1153 See the example in L{Query} on how to supply user name and
1154 password.
1155
1156 @param name: a unicode string containing the user's name
1157
1158 @return: a L{User <musicbrainz2.model.User>} object
1159
1160 @raise ConnectionError: couldn't connect to server
1161 @raise RequestError: invalid ID or include tags
1162 @raise AuthenticationError: invalid user name and/or password
1163 @raise ResourceNotFoundError: track doesn't exist
1164 @raise ResponseError: server returned invalid data
1165 """
1166 filter = UserFilter(name=name)
1167 result = self._getFromWebService('user', '', None, filter)
1168
1169 if len(result.getUserList()) > 0:
1170 return result.getUserList()[0]
1171 else:
1172 raise ResponseError("response didn't contain user data")
1173
1174
1176 if filter is None:
1177 filterParams = [ ]
1178 else:
1179 filterParams = filter.createParameters()
1180
1181 if include is None:
1182 includeParams = [ ]
1183 else:
1184 includeParams = include.createIncludeTags()
1185
1186 stream = self._ws.get(entity, id_, includeParams, filterParams)
1187 try:
1188 parser = MbXmlParser()
1189 return parser.parse(stream)
1190 except ParseError, e:
1191 raise ResponseError(str(e), e)
1192
1193
1195 """Submit track to PUID mappings.
1196
1197 The C{tracks2puids} parameter has to be a dictionary, with the
1198 keys being MusicBrainz track IDs (either as absolute URIs or
1199 in their 36 character ASCII representation) and the values
1200 being PUIDs (ASCII, 36 characters).
1201
1202 Note that this method only works if a valid user name and
1203 password have been set. See the example in L{Query} on how
1204 to supply authentication data.
1205
1206 @param tracks2puids: a dictionary mapping track IDs to PUIDs
1207
1208 @raise ConnectionError: couldn't connect to server
1209 @raise RequestError: invalid track or PUIDs
1210 @raise AuthenticationError: invalid user name and/or password
1211 """
1212 assert self._clientId is not None, 'Please supply a client ID'
1213 params = [ ]
1214 params.append( ('client', self._clientId.encode('utf-8')) )
1215
1216 for (trackId, puid) in tracks2puids.iteritems():
1217 trackId = mbutils.extractUuid(trackId, 'track')
1218 params.append( ('puid', trackId + ' ' + puid) )
1219
1220 encodedStr = urllib.urlencode(params, True)
1221
1222 self._ws.post('track', '', encodedStr)
1223
1225 """Submit track to ISRC mappings.
1226
1227 The C{tracks2isrcs} parameter has to be a dictionary, with the
1228 keys being MusicBrainz track IDs (either as absolute URIs or
1229 in their 36 character ASCII representation) and the values
1230 being ISRCs (ASCII, 12 characters).
1231
1232 Note that this method only works if a valid user name and
1233 password have been set. See the example in L{Query} on how
1234 to supply authentication data.
1235
1236 @param tracks2isrcs: a dictionary mapping track IDs to ISRCs
1237
1238 @raise ConnectionError: couldn't connect to server
1239 @raise RequestError: invalid track or ISRCs
1240 @raise AuthenticationError: invalid user name and/or password
1241 """
1242 params = [ ]
1243
1244 for (trackId, isrc) in tracks2isrcs.iteritems():
1245 trackId = mbutils.extractUuid(trackId, 'track')
1246 params.append( ('isrc', trackId + ' ' + isrc) )
1247
1248 encodedStr = urllib.urlencode(params, True)
1249
1250 self._ws.post('track', '', encodedStr)
1251
1253 """Add releases to a user's collection.
1254
1255 The releases parameter must be a list. It can contain either L{Release}
1256 objects or a string representing a MusicBrainz release ID (either as
1257 absolute URIs or in their 36 character ASCII representation).
1258
1259 Adding a release that is already in the collection has no effect.
1260
1261 @param releases: a list of releases to add to the user collection
1262
1263 @raise ConnectionError: couldn't connect to server
1264 @raise AuthenticationError: invalid user name and/or password
1265 """
1266 rels = []
1267 for release in releases:
1268 if isinstance(release, Release):
1269 rels.append(mbutils.extractUuid(release.id))
1270 else:
1271 rels.append(mbutils.extractUuid(release))
1272 encodedStr = urllib.urlencode({'add': ",".join(rels)}, True)
1273 self._ws.post('collection', '', encodedStr)
1274
1276 """Remove releases from a user's collection.
1277
1278 The releases parameter must be a list. It can contain either L{Release}
1279 objects or a string representing a MusicBrainz release ID (either as
1280 absolute URIs or in their 36 character ASCII representation).
1281
1282 Removing a release that is not in the collection has no effect.
1283
1284 @param releases: a list of releases to remove from the user collection
1285
1286 @raise ConnectionError: couldn't connect to server
1287 @raise AuthenticationError: invalid user name and/or password
1288 """
1289 rels = []
1290 for release in releases:
1291 if isinstance(release, Release):
1292 rels.append(mbutils.extractUuid(release.id))
1293 else:
1294 rels.append(mbutils.extractUuid(release))
1295 encodedStr = urllib.urlencode({'remove': ",".join(rels)}, True)
1296 self._ws.post('collection', '', encodedStr)
1297
1299 """Get the releases that are in a user's collection
1300
1301 A maximum of 100 items will be returned for any one call
1302 to this method. To fetch more than 100 items, use the offset
1303 parameter.
1304
1305 @param offset: the offset to start fetching results from
1306 @param maxitems: the upper limit on items to return
1307
1308 @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects
1309
1310 @raise ConnectionError: couldn't connect to server
1311 @raise AuthenticationError: invalid user name and/or password
1312 """
1313 params = { 'offset': offset, 'maxitems': maxitems }
1314
1315 stream = self._ws.get('collection', '', filter=params)
1316 try:
1317 parser = MbXmlParser()
1318 result = parser.parse(stream)
1319 except ParseError, e:
1320 raise ResponseError(str(e), e)
1321
1322 return result.getReleaseResults()
1323
1352
1353
1387
1389 """Submit rating for an entity.
1390
1391 Note that all previously existing rating from the authenticated
1392 user are replaced with the one given to this method. Other
1393 users' ratings are not affected.
1394
1395 @param entityUri: a string containing an absolute MB ID
1396 @param rating: A L{Rating <musicbrainz2.model.Rating>} object
1397 or integer
1398
1399 @raise ValueError: invalid entityUri
1400 @raise ConnectionError: couldn't connect to server
1401 @raise RequestError: invalid ID, entity or tags
1402 @raise AuthenticationError: invalid user name and/or password
1403 """
1404 entity = mbutils.extractEntityType(entityUri)
1405 uuid = mbutils.extractUuid(entityUri, entity)
1406 params = (
1407 ('type', 'xml'),
1408 ('entity', entity),
1409 ('id', uuid),
1410 ('rating', unicode(rating).encode('utf-8'))
1411 )
1412
1413 encodedStr = urllib.urlencode(params)
1414
1415 self._ws.post('rating', '', encodedStr)
1416
1417
1419 """Return the rating a user has applied to an entity.
1420
1421 The given parameter has to be a fully qualified MusicBrainz
1422 ID, as returned by other library functions.
1423
1424 Note that this method only works if a valid user name and
1425 password have been set. Only the rating the authenticated user
1426 applied to the entity will be returned. If username and/or
1427 password are incorrect, an AuthenticationError is raised.
1428
1429 This method will return a L{Rating <musicbrainz2.model.Rating>}
1430 object.
1431
1432 @param entityUri: a string containing an absolute MB ID
1433
1434 @raise ValueError: invalid entityUri
1435 @raise ConnectionError: couldn't connect to server
1436 @raise RequestError: invalid ID or entity
1437 @raise AuthenticationError: invalid user name and/or password
1438 """
1439 entity = mbutils.extractEntityType(entityUri)
1440 uuid = mbutils.extractUuid(entityUri, entity)
1441 params = { 'entity': entity, 'id': uuid }
1442
1443 stream = self._ws.get('rating', '', filter=params)
1444 try:
1445 parser = MbXmlParser()
1446 result = parser.parse(stream)
1447 except ParseError, e:
1448 raise ResponseError(str(e), e)
1449
1450 return result.getRating()
1451
1453 """Submit a CD Stub to the database.
1454
1455 The number of tracks added to the CD Stub must match the TOC and DiscID
1456 otherwise the submission wil fail. The submission will also fail if
1457 the Disc ID is already in the MusicBrainz database.
1458
1459 This method will only work if no user name and password are set.
1460
1461 @param cdstub: a L{CDStub} object to submit
1462
1463 @raise RequestError: Missmatching TOC/Track information or the
1464 the CD Stub already exists or the Disc ID already exists
1465 """
1466 assert self._clientId is not None, 'Please supply a client ID'
1467 disc = cdstub._disc
1468 params = [ ]
1469 params.append( ('client', self._clientId.encode('utf-8')) )
1470 params.append( ('discid', disc.id) )
1471 params.append( ('title', cdstub.title) )
1472 params.append( ('artist', cdstub.artist) )
1473 if cdstub.barcode != "":
1474 params.append( ('barcode', cdstub.barcode) )
1475 if cdstub.comment != "":
1476 params.append( ('comment', cdstub.comment) )
1477
1478 trackind = 0
1479 for track,artist in cdstub.tracks:
1480 params.append( ('track%d' % trackind, track) )
1481 if artist != "":
1482 params.append( ('artist%d' % trackind, artist) )
1483
1484 trackind += 1
1485
1486 toc = "%d %d %d " % (disc.firstTrackNum, disc.lastTrackNum, disc.sectors)
1487 toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) )
1488
1489 params.append( ('toc', toc) )
1490
1491 encodedStr = urllib.urlencode(params)
1492 self._ws.post('release', '', encodedStr)
1493
1495 selected = filter(lambda x: x[1] == True, tagMap.items())
1496 return map(lambda x: x[0], selected)
1497
1499 """Remove (x, None) tuples and encode (x, str/unicode) to utf-8."""
1500 ret = [ ]
1501 for p in params:
1502 if isinstance(p[1], (str, unicode)):
1503 ret.append( (p[0], p[1].encode('utf-8')) )
1504 elif p[1] is not None:
1505 ret.append(p)
1506
1507 return ret
1508
1510 """Check if the query parameter collides with other parameters."""
1511 tmp = [ ]
1512 for name, value in params:
1513 if value is not None and name not in ('offset', 'limit'):
1514 tmp.append(name)
1515
1516 if 'query' in tmp and len(tmp) > 1:
1517 return False
1518 else:
1519 return True
1520
1521 if __name__ == '__main__':
1522 import doctest
1523 doctest.testmod()
1524
1525
1526