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