Package musicbrainz2 :: Module webservice
[frames] | no frames]

Source Code for Module musicbrainz2.webservice

   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   
40 -class IWebService(object):
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
89 -class WebServiceError(Exception):
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
112 - def __str__(self):
113 """Makes this class printable. 114 115 @return: a string containing an error message 116 """ 117 return self.msg
118 119
120 -class ConnectionError(WebServiceError):
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
130 -class RequestError(WebServiceError):
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
140 -class ResourceNotFoundError(WebServiceError):
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
149 -class AuthenticationError(WebServiceError):
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
158 -class ResponseError(WebServiceError):
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
167 -class DigestAuthHandler(urllib2.HTTPDigestAuthHandler):
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 """
176 - def get_authorization(self, req, chal):
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
183 -class WebService(IWebService):
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, (), # no host set 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
248 - def _openUrl(self, url, data=None):
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: # in python 2.4: httplib.BAD_REQUEST 279 raise RequestError(str(e), e) 280 elif e.code == 401: # httplib.UNAUTHORIZED 281 raise AuthenticationError(str(e), e) 282 elif e.code == 404: # httplib.NOT_FOUND 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: # in python 2.4: httplib.BAD_REQUEST 314 raise RequestError(str(e), e) 315 elif e.code == 401: # httplib.UNAUTHORIZED 316 raise AuthenticationError(str(e), e) 317 elif e.code == 404: # httplib.NOT_FOUND 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 # Special password manager which also works with redirects by simply 327 # ignoring the URI. As a consequence, only *ONE* (username, password) 328 # tuple per realm can be used for all URIs. 329 #
330 - class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr):
331 - def __init__(self):
332 self._realms = { }
333
334 - def find_user_password(self, realm, uri):
335 # ignoring the uri parameter intentionally 336 try: 337 return self._realms[realm] 338 except KeyError: 339 return (None, None)
340
341 - def add_password(self, realm, uri, username, password):
342 # ignoring the uri parameter intentionally 343 self._realms[realm] = (username, password)
344 345
346 -class IFilter(object):
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 """
359 - def createParameters(self):
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
373 -class ArtistFilter(IFilter):
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
398 - def createParameters(self):
399 return _createParameters(self._params)
400 401
402 -class LabelFilter(IFilter):
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
427 - def createParameters(self):
428 return _createParameters(self._params)
429
430 -class ReleaseGroupFilter(IFilter):
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
485 - def createParameters(self):
486 return _createParameters(self._params)
487 488
489 -class ReleaseFilter(IFilter):
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
550 - def createParameters(self):
551 return _createParameters(self._params)
552 553
554 -class TrackFilter(IFilter):
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
601 - def createParameters(self):
602 return _createParameters(self._params)
603 604
605 -class UserFilter(IFilter):
606 """A filter for the user collection.""" 607
608 - def __init__(self, name=None):
609 """Constructor. 610 611 @param name: a unicode string containing a MusicBrainz user name 612 """ 613 self._name = name
614
615 - def createParameters(self):
616 if self._name is not None: 617 return [ ('name', self._name.encode('utf-8')) ] 618 else: 619 return [ ]
620 621
622 -class IIncludes(object):
623 """An interface implemented by include tag generators."""
624 - def createIncludeTags(self):
625 raise NotImplementedError()
626 627
628 -class ArtistIncludes(IIncludes):
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
673 - def createIncludeTags(self):
674 return _createIncludes(self._includes)
675 676
677 -class ReleaseIncludes(IIncludes):
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 # Requesting labels without releaseEvents makes no sense, 703 # so we pull in releaseEvents, if necessary. 704 if labels and not releaseEvents: 705 self._includes['release-events'] = True 706 # Ditto for isrcs with no tracks 707 if isrcs and not tracks: 708 self._includes['tracks'] = True
709
710 - def createIncludeTags(self):
711 return _createIncludes(self._includes)
712 713
714 -class ReleaseGroupIncludes(IIncludes):
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
728 - def createIncludeTags(self):
729 return _createIncludes(self._includes)
730 731
732 -class TrackIncludes(IIncludes):
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
751 - def createIncludeTags(self):
752 return _createIncludes(self._includes)
753 754
755 -class LabelIncludes(IIncludes):
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
764 - def createIncludeTags(self):
765 return _createIncludes(self._includes)
766 767
768 -class Query(object):
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
921 - def __init__(self, ws=None, wsFactory=WebService, clientId=None):
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
954 - def getArtistById(self, id_, include=None):
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
980 - def getArtists(self, filter):
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
994 - def getLabelById(self, id_, include=None):
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
1015 - def getLabels(self, filter):
1016 result = self._getFromWebService('label', '', filter=filter) 1017 return result.getLabelResults()
1018
1019 - def getReleaseById(self, id_, include=None):
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
1045 - def getReleases(self, filter):
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
1059 - def getReleaseGroupById(self, id_, include=None):
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
1084 - def getReleaseGroups(self, filter):
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
1098 - def getTrackById(self, id_, include=None):
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
1124 - def getTracks(self, filter):
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
1139 - def getUserByName(self, name):
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
1168 - def _getFromWebService(self, entity, id_, include=None, filter=None):
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
1187 - def submitPuids(self, tracks2puids):
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
1217 - def submitISRCs(self, tracks2isrcs):
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
1245 - def addToUserCollection(self, releases):
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
1268 - def removeFromUserCollection(self, releases):
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
1291 - def getUserCollection(self, offset=0, maxitems=100):
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
1318 - def submitUserTags(self, entityUri, tags):
1319 """Submit folksonomy tags for an entity. 1320 1321 Note that all previously existing tags from the authenticated 1322 user are replaced with the ones given to this method. Other 1323 users' tags are not affected. 1324 1325 @param entityUri: a string containing an absolute MB ID 1326 @param tags: A list of either L{Tag <musicbrainz2.model.Tag>} objects 1327 or strings 1328 1329 @raise ValueError: invalid entityUri 1330 @raise ConnectionError: couldn't connect to server 1331 @raise RequestError: invalid ID, entity or tags 1332 @raise AuthenticationError: invalid user name and/or password 1333 """ 1334 entity = mbutils.extractEntityType(entityUri) 1335 uuid = mbutils.extractUuid(entityUri, entity) 1336 params = ( 1337 ('type', 'xml'), 1338 ('entity', entity), 1339 ('id', uuid), 1340 ('tags', ','.join([unicode(tag).encode('utf-8') for tag in tags])) 1341 ) 1342 1343 encodedStr = urllib.urlencode(params) 1344 1345 self._ws.post('tag', '', encodedStr)
1346 1347
1348 - def getUserTags(self, entityUri):
1349 """Returns a list of folksonomy tags a user has applied to an entity. 1350 1351 The given parameter has to be a fully qualified MusicBrainz ID, as 1352 returned by other library functions. 1353 1354 Note that this method only works if a valid user name and 1355 password have been set. Only the tags the authenticated user 1356 applied to the entity will be returned. If username and/or 1357 password are incorrect, an AuthenticationError is raised. 1358 1359 This method will return a list of L{Tag <musicbrainz2.model.Tag>} 1360 objects. 1361 1362 @param entityUri: a string containing an absolute MB ID 1363 1364 @raise ValueError: invalid entityUri 1365 @raise ConnectionError: couldn't connect to server 1366 @raise RequestError: invalid ID or entity 1367 @raise AuthenticationError: invalid user name and/or password 1368 """ 1369 entity = mbutils.extractEntityType(entityUri) 1370 uuid = mbutils.extractUuid(entityUri, entity) 1371 params = { 'entity': entity, 'id': uuid } 1372 1373 stream = self._ws.get('tag', '', filter=params) 1374 try: 1375 parser = MbXmlParser() 1376 result = parser.parse(stream) 1377 except ParseError, e: 1378 raise ResponseError(str(e), e) 1379 1380 return result.getTagList()
1381
1382 - def submitUserRating(self, entityUri, rating):
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
1412 - def getUserRating(self, entityUri):
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
1446 - def submitCDStub(self, cdstub):
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
1488 -def _createIncludes(tagMap):
1489 selected = filter(lambda x: x[1] == True, tagMap.items()) 1490 return map(lambda x: x[0], selected)
1491
1492 -def _createParameters(params):
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
1503 -def _paramsValid(params):
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 # EOF 1520