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