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