MythTV  master
musicbrainz.py
Go to the documentation of this file.
1 # This file is part of the musicbrainzngs library
2 # Copyright (C) Alastair Porter, Adrian Sampson, and others
3 # This file is distributed under a BSD-2-Clause type license.
4 # See the COPYING file for more information.
5 
6 import re
7 import threading
8 import time
9 import logging
10 import socket
11 import hashlib
12 import locale
13 import sys
14 import json
15 import xml.etree.ElementTree as etree
16 from xml.parsers import expat
17 from warnings import warn
18 
19 from musicbrainzngs import mbxml
20 from musicbrainzngs import util
21 from musicbrainzngs import compat
22 
23 _version = "0.6dev"
24 _log = logging.getLogger("musicbrainzngs")
25 
26 LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
27 
28 # Constants for validation.
29 
30 RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'event', 'recording', 'release', 'release-group', 'series', 'url', 'work', 'instrument']
31 RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES]
32 TAG_INCLUDES = ["tags", "user-tags"]
33 RATING_INCLUDES = ["ratings", "user-ratings"]
34 
35 VALID_INCLUDES = {
36  'area' : ["aliases", "annotation"] + RELATION_INCLUDES,
37  'artist': [
38  "recordings", "releases", "release-groups", "works", # Subqueries
39  "various-artists", "discids", "media", "isrcs",
40  "aliases", "annotation"
41  ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
42  'annotation': [
43 
44  ],
45  'instrument': ["aliases", "annotation"
46  ] + RELATION_INCLUDES + TAG_INCLUDES,
47  'label': [
48  "releases", # Subqueries
49  "discids", "media",
50  "aliases", "annotation"
51  ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
52  'place' : ["aliases", "annotation"] + RELATION_INCLUDES + TAG_INCLUDES,
53  'event' : ["aliases"] + RELATION_INCLUDES,
54  'recording': [
55  "artists", "releases", # Subqueries
56  "discids", "media", "artist-credits", "isrcs",
57  "annotation", "aliases"
58  ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
59  'release': [
60  "artists", "labels", "recordings", "release-groups", "media",
61  "artist-credits", "discids", "puids", "isrcs",
62  "recording-level-rels", "work-level-rels", "annotation", "aliases"
63  ] + TAG_INCLUDES + RELATION_INCLUDES,
64  'release-group': [
65  "artists", "releases", "discids", "media",
66  "artist-credits", "annotation", "aliases"
67  ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
68  'series': [
69  "annotation", "aliases"
70  ] + RELATION_INCLUDES,
71  'work': [
72  "artists", # Subqueries
73  "aliases", "annotation"
74  ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
75  'url': RELATION_INCLUDES,
76  'discid': [ # Discid should be the same as release
77  "artists", "labels", "recordings", "release-groups", "media",
78  "artist-credits", "discids", "puids", "isrcs",
79  "recording-level-rels", "work-level-rels", "annotation", "aliases"
80  ] + RELATION_INCLUDES,
81  'isrc': ["artists", "releases", "puids", "isrcs"],
82  'iswc': ["artists"],
83  'collection': ['releases'],
84 }
85 VALID_BROWSE_INCLUDES = {
86  'releases': ["artist-credits", "labels", "recordings", "isrcs",
87  "release-groups", "media", "discids"] + RELATION_INCLUDES,
88  'recordings': ["artist-credits", "isrcs"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
89  'labels': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
90  'artists': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
91  'events': ["aliases"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
92  'urls': RELATION_INCLUDES,
93  'release-groups': ["artist-credits"] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES
94 }
95 
96 #: These can be used to filter whenever releases are includes or browsed
97 VALID_RELEASE_TYPES = [
98  "nat",
99  "album", "single", "ep", "broadcast", "other", # primary types
100  "compilation", "soundtrack", "spokenword", "interview", "audiobook",
101  "live", "remix", "dj-mix", "mixtape/street", # secondary types
102 ]
103 #: These can be used to filter whenever releases or release-groups are involved
104 VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
105 VALID_SEARCH_FIELDS = {
106  'annotation': [
107  'entity', 'name', 'text', 'type'
108  ],
109  'area': [
110  'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended',
111  'iso', 'iso1', 'iso2', 'iso3', 'type'
112  ],
113  'artist': [
114  'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment',
115  'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type',
116  'area', 'beginarea', 'endarea'
117  ],
118  'label': [
119  'alias', 'begin', 'code', 'comment', 'country', 'end', 'ended',
120  'ipi', 'label', 'labelaccent', 'laid', 'sortname', 'type', 'tag',
121  'area'
122  ],
123  'recording': [
124  'arid', 'artist', 'artistname', 'creditname', 'comment',
125  'country', 'date', 'dur', 'format', 'isrc', 'number',
126  'position', 'primarytype', 'puid', 'qdur', 'recording',
127  'recordingaccent', 'reid', 'release', 'rgid', 'rid',
128  'secondarytype', 'status', 'tnum', 'tracks', 'tracksrelease',
129  'tag', 'type', 'video'
130  ],
131  'release-group': [
132  'arid', 'artist', 'artistname', 'comment', 'creditname',
133  'primarytype', 'rgid', 'releasegroup', 'releasegroupaccent',
134  'releases', 'release', 'reid', 'secondarytype', 'status',
135  'tag', 'type'
136  ],
137  'release': [
138  'arid', 'artist', 'artistname', 'asin', 'barcode', 'creditname',
139  'catno', 'comment', 'country', 'creditname', 'date', 'discids',
140  'discidsmedium', 'format', 'laid', 'label', 'lang', 'mediums',
141  'primarytype', 'puid', 'quality', 'reid', 'release', 'releaseaccent',
142  'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks',
143  'tracksmedium', 'type'
144  ],
145  'series': [
146  'alias', 'comment', 'sid', 'series', 'type'
147  ],
148  'work': [
149  'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag',
150  'type', 'wid', 'work', 'workaccent'
151  ],
152 }
153 
154 # Constants
155 class AUTH_YES: pass
156 class AUTH_NO: pass
157 class AUTH_IFSET: pass
158 
159 
160 # Exceptions.
161 
162 class MusicBrainzError(Exception):
163  """Base class for all exceptions related to MusicBrainz."""
164  pass
165 
167  """Error related to misuse of the module API."""
168  pass
169 
171  pass
172 
174  def __init__(self, msg='Invalid Includes', reason=None):
175  super(InvalidIncludeError, self).__init__(self)
176  self.msg = msg
177  self.reason = reason
178 
179  def __str__(self):
180  return self.msg
181 
183  def __init__(self, msg='Invalid Includes', reason=None):
184  super(InvalidFilterError, self).__init__(self)
185  self.msg = msg
186  self.reason = reason
187 
188  def __str__(self):
189  return self.msg
190 
192  """Error related to MusicBrainz API requests."""
193  def __init__(self, message=None, cause=None):
194  """Pass ``cause`` if this exception was caused by another
195  exception.
196  """
197  self.message = message
198  self.cause = cause
199 
200  def __str__(self):
201  if self.message:
202  msg = "%s, " % self.message
203  else:
204  msg = ""
205  msg += "caused by: %s" % str(self.cause)
206  return msg
207 
209  """Problem communicating with the MB server."""
210  pass
211 
212 class ResponseError(WebServiceError):
213  """Bad response sent by the MB server."""
214  pass
215 
217  """Received a HTTP 401 response while accessing a protected resource."""
218  pass
219 
220 
221 # Helpers for validating and formatting allowed sets.
222 
223 def _check_includes_impl(includes, valid_includes):
224  for i in includes:
225  if i not in valid_includes:
226  raise InvalidIncludeError("Bad includes: "
227  "%s is not a valid include" % i)
228 def _check_includes(entity, inc):
229  _check_includes_impl(inc, VALID_INCLUDES[entity])
230 
231 def _check_filter(values, valid):
232  for v in values:
233  if v not in valid:
234  raise InvalidFilterError(v)
235 
236 def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[]):
237  """Check that the status or type values are valid. Then, check that
238  the filters can be used with the given includes. Return a params
239  dict that can be passed to _do_mb_query.
240  """
241  if isinstance(release_status, compat.basestring):
242  release_status = [release_status]
243  if isinstance(release_type, compat.basestring):
244  release_type = [release_type]
245  _check_filter(release_status, VALID_RELEASE_STATUSES)
246  _check_filter(release_type, VALID_RELEASE_TYPES)
247 
248  if (release_status
249  and "releases" not in includes and entity != "release"):
250  raise InvalidFilterError("Can't have a status with no release include")
251  if (release_type
252  and "release-groups" not in includes and "releases" not in includes
253  and entity not in ["release-group", "release"]):
254  raise InvalidFilterError("Can't have a release type "
255  "with no releases or release-groups involved")
256 
257  # Build parameters.
258  params = {}
259  if len(release_status):
260  params["status"] = "|".join(release_status)
261  if len(release_type):
262  params["type"] = "|".join(release_type)
263  return params
264 
265 def _docstring(entity, browse=False):
266  def _decorator(func):
267  if browse:
268  includes = list(VALID_BROWSE_INCLUDES.get(entity, []))
269  else:
270  includes = list(VALID_INCLUDES.get(entity, []))
271  # puids are allowed so nothing breaks, but not documented
272  if "puids" in includes: includes.remove("puids")
273  includes = ", ".join(includes)
274  if func.__doc__:
275  search_fields = list(VALID_SEARCH_FIELDS.get(entity, []))
276  # puid is allowed so nothing breaks, but not documented
277  if "puid" in search_fields: search_fields.remove("puid")
278  func.__doc__ = func.__doc__.format(includes=includes,
279  fields=", ".join(search_fields))
280  return func
281 
282  return _decorator
283 
284 
285 # Global authentication and endpoint details.
286 
287 user = password = ""
288 hostname = "musicbrainz.org"
289 _client = ""
290 _useragent = ""
291 
292 def auth(u, p):
293  """Set the username and password to be used in subsequent queries to
294  the MusicBrainz XML API that require authentication.
295  """
296  global user, password
297  user = u
298  password = p
299 
300 def set_useragent(app, version, contact=None):
301  """Set the User-Agent to be used for requests to the MusicBrainz webservice.
302  This must be set before requests are made."""
303  global _useragent, _client
304  if not app or not version:
305  raise ValueError("App and version can not be empty")
306  if contact is not None:
307  _useragent = "%s/%s python-musicbrainzngs/%s ( %s )" % (app, version, _version, contact)
308  else:
309  _useragent = "%s/%s python-musicbrainzngs/%s" % (app, version, _version)
310  _client = "%s-%s" % (app, version)
311  _log.debug("set user-agent to %s" % _useragent)
312 
313 def set_hostname(new_hostname):
314  """Set the hostname for MusicBrainz webservice requests.
315  Defaults to 'musicbrainz.org'.
316  You can also include a port: 'localhost:8000'."""
317  global hostname
318  hostname = new_hostname
319 
320 # Rate limiting.
321 
322 limit_interval = 1.0
323 limit_requests = 1
324 do_rate_limit = True
325 
326 def set_rate_limit(limit_or_interval=1.0, new_requests=1):
327  """Sets the rate limiting behavior of the module. Must be invoked
328  before the first Web service call.
329  If the `limit_or_interval` parameter is set to False then
330  rate limiting will be disabled. If it is a number then only
331  a set number of requests (`new_requests`) will be made per
332  given interval (`limit_or_interval`).
333  """
334  global limit_interval
335  global limit_requests
336  global do_rate_limit
337  if isinstance(limit_or_interval, bool):
338  do_rate_limit = limit_or_interval
339  else:
340  if limit_or_interval <= 0.0:
341  raise ValueError("limit_or_interval can't be less than 0")
342  if new_requests <= 0:
343  raise ValueError("new_requests can't be less than 0")
344  do_rate_limit = True
345  limit_interval = limit_or_interval
346  limit_requests = new_requests
347 
348 class _rate_limit(object):
349  """A decorator that limits the rate at which the function may be
350  called. The rate is controlled by the `limit_interval` and
351  `limit_requests` global variables. The limiting is thread-safe;
352  only one thread may be in the function at a time (acts like a
353  monitor in this sense). The globals must be set before the first
354  call to the limited function.
355  """
356  def __init__(self, fun):
357  self.fun = fun
358  self.last_call = 0.0
359  self.lock = threading.Lock()
360  self.remaining_requests = None # Set on first invocation.
361 
362  def _update_remaining(self):
363  """Update remaining requests based on the elapsed time since
364  they were last calculated.
365  """
366  # On first invocation, we have the maximum number of requests
367  # available.
368  if self.remaining_requests is None:
369  self.remaining_requests = float(limit_requests)
370 
371  else:
372  since_last_call = time.time() - self.last_call
373  self.remaining_requests += since_last_call * \
374  (limit_requests / limit_interval)
375  self.remaining_requests = min(self.remaining_requests,
376  float(limit_requests))
377 
378  self.last_call = time.time()
379 
380  def __call__(self, *args, **kwargs):
381  with self.lock:
382  if do_rate_limit:
383  self._update_remaining()
384 
385  # Delay if necessary.
386  while self.remaining_requests < 0.999:
387  time.sleep((1.0 - self.remaining_requests) *
388  (limit_requests / limit_interval))
389  self._update_remaining()
390 
391  # Call the original function, "paying" for this call.
392  self.remaining_requests -= 1.0
393  return self.fun(*args, **kwargs)
394 
395 # From pymb2
396 class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
397  def __init__(self):
398  self._realms = { }
399 
400  def find_user_password(self, realm, uri):
401  # ignoring the uri parameter intentionally
402  try:
403  return self._realms[realm]
404  except KeyError:
405  return (None, None)
406 
407  def add_password(self, realm, uri, username, password):
408  # ignoring the uri parameter intentionally
409  self._realms[realm] = (username, password)
410 
411 class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
412  def get_authorization (self, req, chal):
413  qop = chal.get ('qop', None)
414  if qop and ',' in qop and 'auth' in qop.split (','):
415  chal['qop'] = 'auth'
416 
417  return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal)
418 
419  def _encode_utf8(self, msg):
420  """The MusicBrainz server also accepts UTF-8 encoded passwords."""
421  encoding = sys.stdin.encoding or locale.getpreferredencoding()
422  try:
423  # This works on Python 2 (msg in bytes)
424  msg = msg.decode(encoding)
425  except AttributeError:
426  # on Python 3 (msg is already in unicode)
427  pass
428  return msg.encode("utf-8")
429 
430  def get_algorithm_impls(self, algorithm):
431  # algorithm should be case-insensitive according to RFC2617
432  algorithm = algorithm.upper()
433  # lambdas assume digest modules are imported at the top level
434  if algorithm == 'MD5':
435  H = lambda x: hashlib.md5(self._encode_utf8(x)).hexdigest()
436  elif algorithm == 'SHA':
437  H = lambda x: hashlib.sha1(self._encode_utf8(x)).hexdigest()
438  # XXX MD5-sess
439  KD = lambda s, d: H("%s:%s" % (s, d))
440  return H, KD
441 
442 class _MusicbrainzHttpRequest(compat.Request):
443  """ A custom request handler that allows DELETE and PUT"""
444  def __init__(self, method, url, data=None):
445  compat.Request.__init__(self, url, data)
446  allowed_m = ["GET", "POST", "DELETE", "PUT"]
447  if method not in allowed_m:
448  raise ValueError("invalid method: %s" % method)
449  self.method = method
450 
451  def get_method(self):
452  return self.method
453 
454 
455 # Core (internal) functions for calling the MB API.
456 
457 def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
458  """Open an HTTP request with a given URL opener and (optionally) a
459  request body. Transient errors lead to retries. Permanent errors
460  and repeated errors are translated into a small set of handleable
461  exceptions. Return a bytestring.
462  """
463  last_exc = None
464  for retry_num in range(max_retries):
465  if retry_num: # Not the first try: delay an increasing amount.
466  _log.info("retrying after delay (#%i)" % retry_num)
467  time.sleep(retry_num * retry_delay_delta)
468 
469  try:
470  if body:
471  f = opener.open(req, body)
472  else:
473  f = opener.open(req)
474  return f.read()
475 
476  except compat.HTTPError as exc:
477  if exc.code in (400, 404, 411):
478  # Bad request, not found, etc.
479  raise ResponseError(cause=exc)
480  elif exc.code in (503, 502, 500):
481  # Rate limiting, internal overloading...
482  _log.info("HTTP error %i" % exc.code)
483  elif exc.code in (401, ):
484  raise AuthenticationError(cause=exc)
485  else:
486  # Other, unknown error. Should handle more cases, but
487  # retrying for now.
488  _log.info("unknown HTTP error %i" % exc.code)
489  last_exc = exc
490  except compat.BadStatusLine as exc:
491  _log.info("bad status line")
492  last_exc = exc
493  except compat.HTTPException as exc:
494  _log.info("miscellaneous HTTP exception: %s" % str(exc))
495  last_exc = exc
496  except compat.URLError as exc:
497  if isinstance(exc.reason, socket.error):
498  code = exc.reason.errno
499  if code == 104: # "Connection reset by peer."
500  continue
501  raise NetworkError(cause=exc)
502  except socket.timeout as exc:
503  _log.info("socket timeout")
504  last_exc = exc
505  except socket.error as exc:
506  if exc.errno == 104:
507  continue
508  raise NetworkError(cause=exc)
509  except IOError as exc:
510  raise NetworkError(cause=exc)
511 
512  # Out of retries!
513  raise NetworkError("retried %i times" % max_retries, last_exc)
514 
515 # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7
516 # and ElementTree 1.3.
517 if hasattr(etree, 'ParseError'):
518  ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
519 else:
520  ETREE_EXCEPTIONS = (expat.ExpatError)
521 
522 
523 # Parsing setup
524 
525 def mb_parser_null(resp):
526  """Return the raw response (XML)"""
527  return resp
528 
529 def mb_parser_xml(resp):
530  """Return a Python dict representing the XML response"""
531  # Parse the response.
532  try:
533  return mbxml.parse_message(resp)
534  except UnicodeError as exc:
535  raise ResponseError(cause=exc)
536  except Exception as exc:
537  if isinstance(exc, ETREE_EXCEPTIONS):
538  raise ResponseError(cause=exc)
539  else:
540  raise
541 
542 # Defaults
543 parser_fun = mb_parser_xml
544 ws_format = "xml"
545 
546 def set_parser(new_parser_fun=None):
547  """Sets the function used to parse the response from the
548  MusicBrainz web service.
549 
550  If no parser is given, the parser is reset to the default parser
551  :func:`mb_parser_xml`.
552  """
553  global parser_fun
554  if new_parser_fun is None:
555  new_parser_fun = mb_parser_xml
556  if not callable(new_parser_fun):
557  raise ValueError("new_parser_fun must be callable")
558  parser_fun = new_parser_fun
559 
560 def set_format(fmt="xml"):
561  """Sets the format that should be returned by the Web Service.
562  The server currently supports `xml` and `json`.
563 
564  This method will set a default parser for the specified format,
565  but you can modify it with :func:`set_parser`.
566 
567  .. warning:: The json format used by the server is different from
568  the json format returned by the `musicbrainzngs` internal parser
569  when using the `xml` format! This format may change at any time.
570  """
571  global ws_format
572  if fmt == "xml":
573  ws_format = fmt
574  set_parser() # set to default
575  elif fmt == "json":
576  ws_format = fmt
577  warn("The json format is non-official and may change at any time")
578  set_parser(json.loads)
579  else:
580  raise ValueError("invalid format: %s" % fmt)
581 
582 
583 @_rate_limit
584 def _mb_request(path, method='GET', auth_required=AUTH_NO,
585  client_required=False, args=None, data=None, body=None):
586  """Makes a request for the specified `path` (endpoint) on /ws/2 on
587  the globally-specified hostname. Parses the responses and returns
588  the resulting object. `auth_required` and `client_required` control
589  whether exceptions should be raised if the username/password and
590  client are left unspecified, respectively.
591  """
592  global parser_fun
593 
594  if args is None:
595  args = {}
596  else:
597  args = dict(args) or {}
598 
599  if _useragent == "":
600  raise UsageError("set a proper user-agent with "
601  "set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")")
602 
603  if client_required:
604  args["client"] = _client
605 
606  if ws_format != "xml":
607  args["fmt"] = ws_format
608 
609  # Convert args from a dictionary to a list of tuples
610  # so that the ordering of elements is stable for easy
611  # testing (in this case we order alphabetically)
612  # Encode Unicode arguments using UTF-8.
613  newargs = []
614  for key, value in sorted(args.items()):
615  if isinstance(value, compat.unicode):
616  value = value.encode('utf8')
617  newargs.append((key, value))
618 
619  # Construct the full URL for the request, including hostname and
620  # query string.
621  url = compat.urlunparse((
622  'http',
623  hostname,
624  '/ws/2/%s' % path,
625  '',
626  compat.urlencode(newargs),
627  ''
628  ))
629  _log.debug("%s request for %s" % (method, url))
630 
631  # Set up HTTP request handler and URL opener.
632  httpHandler = compat.HTTPHandler(debuglevel=0)
633  handlers = [httpHandler]
634 
635  # Add credentials if required.
636  add_auth = False
637  if auth_required == AUTH_YES:
638  _log.debug("Auth required for %s" % url)
639  if not user:
640  raise UsageError("authorization required; "
641  "use auth(user, pass) first")
642  add_auth = True
643 
644  if auth_required == AUTH_IFSET and user:
645  _log.debug("Using auth for %s because user and pass is set" % url)
646  add_auth = True
647 
648  if add_auth:
649  passwordMgr = _RedirectPasswordMgr()
650  authHandler = _DigestAuthHandler(passwordMgr)
651  authHandler.add_password("musicbrainz.org", (), user, password)
652  handlers.append(authHandler)
653 
654  opener = compat.build_opener(*handlers)
655 
656  # Make request.
657  req = _MusicbrainzHttpRequest(method, url, data)
658  req.add_header('User-Agent', _useragent)
659  _log.debug("requesting with UA %s" % _useragent)
660  if body:
661  req.add_header('Content-Type', 'application/xml; charset=UTF-8')
662  elif not data and not req.has_header('Content-Length'):
663  # Explicitly indicate zero content length if no request data
664  # will be sent (avoids HTTP 411 error).
665  req.add_header('Content-Length', '0')
666  resp = _safe_read(opener, req, body)
667 
668  return parser_fun(resp)
669 
670 def _get_auth_type(entity, id, includes):
671  """ Some calls require authentication. This returns
672  True if a call does, False otherwise
673  """
674  if "user-tags" in includes or "user-ratings" in includes:
675  return AUTH_YES
676  elif entity.startswith("collection"):
677  if not id:
678  return AUTH_YES
679  else:
680  return AUTH_IFSET
681  else:
682  return AUTH_NO
683 
684 def _do_mb_query(entity, id, includes=[], params={}):
685  """Make a single GET call to the MusicBrainz XML API. `entity` is a
686  string indicated the type of object to be retrieved. The id may be
687  empty, in which case the query is a search. `includes` is a list
688  of strings that must be valid includes for the entity type. `params`
689  is a dictionary of additional parameters for the API call. The
690  response is parsed and returned.
691  """
692  # Build arguments.
693  if not isinstance(includes, list):
694  includes = [includes]
695  _check_includes(entity, includes)
696  auth_required = _get_auth_type(entity, id, includes)
697  args = dict(params)
698  if len(includes) > 0:
699  inc = " ".join(includes)
700  args["inc"] = inc
701 
702  # Build the endpoint components.
703  path = '%s/%s' % (entity, id)
704  return _mb_request(path, 'GET', auth_required, args=args)
705 
706 def _do_mb_search(entity, query='', fields={},
707  limit=None, offset=None, strict=False):
708  """Perform a full-text search on the MusicBrainz search server.
709  `query` is a lucene query string when no fields are set,
710  but is escaped when any fields are given. `fields` is a dictionary
711  of key/value query parameters. They keys in `fields` must be valid
712  for the given entity type.
713  """
714  # Encode the query terms as a Lucene query string.
715  query_parts = []
716  if query:
717  clean_query = util._unicode(query)
718  if fields:
719  clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
720  clean_query)
721  if strict:
722  query_parts.append('"%s"' % clean_query)
723  else:
724  query_parts.append(clean_query.lower())
725  else:
726  query_parts.append(clean_query)
727  for key, value in fields.items():
728  # Ensure this is a valid search field.
729  if key not in VALID_SEARCH_FIELDS[entity]:
731  '%s is not a valid search field for %s' % (key, entity)
732  )
733  elif key == "puid":
734  warn("PUID support was removed from server\n"
735  "the 'puid' field is ignored",
736  Warning, stacklevel=2)
737 
738  # Escape Lucene's special characters.
739  value = util._unicode(value)
740  value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
741  if value:
742  if strict:
743  query_parts.append('%s:"%s"' % (key, value))
744  else:
745  value = value.lower() # avoid AND / OR
746  query_parts.append('%s:(%s)' % (key, value))
747  if strict:
748  full_query = ' AND '.join(query_parts).strip()
749  else:
750  full_query = ' '.join(query_parts).strip()
751 
752  if not full_query:
753  raise ValueError('at least one query term is required')
754 
755  # Additional parameters to the search.
756  params = {'query': full_query}
757  if limit:
758  params['limit'] = str(limit)
759  if offset:
760  params['offset'] = str(offset)
761 
762  return _do_mb_query(entity, '', [], params)
763 
764 def _do_mb_delete(path):
765  """Send a DELETE request for the specified object.
766  """
767  return _mb_request(path, 'DELETE', AUTH_YES, True)
768 
769 def _do_mb_put(path):
770  """Send a PUT request for the specified object.
771  """
772  return _mb_request(path, 'PUT', AUTH_YES, True)
773 
774 def _do_mb_post(path, body):
775  """Perform a single POST call for an endpoint with a specified
776  request body.
777  """
778  return _mb_request(path, 'POST', AUTH_YES, True, body=body)
779 
780 
781 # The main interface!
782 
783 # Single entity by ID
784 
785 @_docstring('area')
786 def get_area_by_id(id, includes=[], release_status=[], release_type=[]):
787  """Get the area with the MusicBrainz `id` as a dict with an 'area' key.
788 
789  *Available includes*: {includes}"""
790  params = _check_filter_and_make_params("area", includes,
791  release_status, release_type)
792  return _do_mb_query("area", id, includes, params)
793 
794 @_docstring('artist')
795 def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
796  """Get the artist with the MusicBrainz `id` as a dict with an 'artist' key.
797 
798  *Available includes*: {includes}"""
799  params = _check_filter_and_make_params("artist", includes,
800  release_status, release_type)
801  return _do_mb_query("artist", id, includes, params)
802 
803 @_docstring('instrument')
804 def get_instrument_by_id(id, includes=[], release_status=[], release_type=[]):
805  """Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key.
806 
807  *Available includes*: {includes}"""
808  params = _check_filter_and_make_params("instrument", includes,
809  release_status, release_type)
810  return _do_mb_query("instrument", id, includes, params)
811 
812 @_docstring('label')
813 def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
814  """Get the label with the MusicBrainz `id` as a dict with a 'label' key.
815 
816  *Available includes*: {includes}"""
817  params = _check_filter_and_make_params("label", includes,
818  release_status, release_type)
819  return _do_mb_query("label", id, includes, params)
820 
821 @_docstring('place')
822 def get_place_by_id(id, includes=[], release_status=[], release_type=[]):
823  """Get the place with the MusicBrainz `id` as a dict with an 'place' key.
824 
825  *Available includes*: {includes}"""
826  params = _check_filter_and_make_params("place", includes,
827  release_status, release_type)
828  return _do_mb_query("place", id, includes, params)
829 
830 @_docstring('event')
831 def get_event_by_id(id, includes=[], release_status=[], release_type=[]):
832  """Get the event with the MusicBrainz `id` as a dict with an 'event' key.
833 
834  The event dict has the following keys:
835  `id`, `type`, `name`, `time`, `disambiguation` and `life-span`.
836 
837  *Available includes*: {includes}"""
838  params = _check_filter_and_make_params("event", includes,
839  release_status, release_type)
840  return _do_mb_query("event", id, includes, params)
841 
842 @_docstring('recording')
843 def get_recording_by_id(id, includes=[], release_status=[], release_type=[]):
844  """Get the recording with the MusicBrainz `id` as a dict
845  with a 'recording' key.
846 
847  *Available includes*: {includes}"""
848  params = _check_filter_and_make_params("recording", includes,
849  release_status, release_type)
850  return _do_mb_query("recording", id, includes, params)
851 
852 @_docstring('release')
853 def get_release_by_id(id, includes=[], release_status=[], release_type=[]):
854  """Get the release with the MusicBrainz `id` as a dict with a 'release' key.
855 
856  *Available includes*: {includes}"""
857  params = _check_filter_and_make_params("release", includes,
858  release_status, release_type)
859  return _do_mb_query("release", id, includes, params)
860 
861 @_docstring('release-group')
862 def get_release_group_by_id(id, includes=[],
863  release_status=[], release_type=[]):
864  """Get the release group with the MusicBrainz `id` as a dict
865  with a 'release-group' key.
866 
867  *Available includes*: {includes}"""
868  params = _check_filter_and_make_params("release-group", includes,
869  release_status, release_type)
870  return _do_mb_query("release-group", id, includes, params)
871 
872 @_docstring('series')
873 def get_series_by_id(id, includes=[]):
874  """Get the series with the MusicBrainz `id` as a dict with a 'series' key.
875 
876  *Available includes*: {includes}"""
877  return _do_mb_query("series", id, includes)
878 
879 @_docstring('work')
880 def get_work_by_id(id, includes=[]):
881  """Get the work with the MusicBrainz `id` as a dict with a 'work' key.
882 
883  *Available includes*: {includes}"""
884  return _do_mb_query("work", id, includes)
885 
886 @_docstring('url')
887 def get_url_by_id(id, includes=[]):
888  """Get the url with the MusicBrainz `id` as a dict with a 'url' key.
889 
890  *Available includes*: {includes}"""
891  return _do_mb_query("url", id, includes)
892 
893 
894 # Searching
895 
896 @_docstring('annotation')
897 def search_annotations(query='', limit=None, offset=None, strict=False, **fields):
898  """Search for annotations and return a dict with an 'annotation-list' key.
899 
900  *Available search fields*: {fields}"""
901  return _do_mb_search('annotation', query, fields, limit, offset, strict)
902 
903 @_docstring('area')
904 def search_areas(query='', limit=None, offset=None, strict=False, **fields):
905  """Search for areas and return a dict with an 'area-list' key.
906 
907  *Available search fields*: {fields}"""
908  return _do_mb_search('area', query, fields, limit, offset, strict)
909 
910 @_docstring('artist')
911 def search_artists(query='', limit=None, offset=None, strict=False, **fields):
912  """Search for artists and return a dict with an 'artist-list' key.
913 
914  *Available search fields*: {fields}"""
915  return _do_mb_search('artist', query, fields, limit, offset, strict)
916 
917 @_docstring('event')
918 def search_events(query='', limit=None, offset=None, strict=False, **fields):
919  """Search for events and return a dict with an 'event-list' key.
920 
921  *Available search fields*: {fields}"""
922  return _do_mb_search('event', query, fields, limit, offset, strict)
923 
924 @_docstring('instrument')
925 def search_instruments(query='', limit=None, offset=None, strict=False, **fields):
926  """Search for instruments and return a dict with a 'instrument-list' key.
927 
928  *Available search fields*: {fields}"""
929  return _do_mb_search('instrument', query, fields, limit, offset, strict)
930 
931 @_docstring('label')
932 def search_labels(query='', limit=None, offset=None, strict=False, **fields):
933  """Search for labels and return a dict with a 'label-list' key.
934 
935  *Available search fields*: {fields}"""
936  return _do_mb_search('label', query, fields, limit, offset, strict)
937 
938 @_docstring('recording')
939 def search_recordings(query='', limit=None, offset=None,
940  strict=False, **fields):
941  """Search for recordings and return a dict with a 'recording-list' key.
942 
943  *Available search fields*: {fields}"""
944  return _do_mb_search('recording', query, fields, limit, offset, strict)
945 
946 @_docstring('release')
947 def search_releases(query='', limit=None, offset=None, strict=False, **fields):
948  """Search for recordings and return a dict with a 'recording-list' key.
949 
950  *Available search fields*: {fields}"""
951  return _do_mb_search('release', query, fields, limit, offset, strict)
952 
953 @_docstring('release-group')
954 def search_release_groups(query='', limit=None, offset=None,
955  strict=False, **fields):
956  """Search for release groups and return a dict
957  with a 'release-group-list' key.
958 
959  *Available search fields*: {fields}"""
960  return _do_mb_search('release-group', query, fields, limit, offset, strict)
961 
962 @_docstring('series')
963 def search_series(query='', limit=None, offset=None, strict=False, **fields):
964  """Search for series and return a dict with a 'series-list' key.
965 
966  *Available search fields*: {fields}"""
967  return _do_mb_search('series', query, fields, limit, offset, strict)
968 
969 @_docstring('work')
970 def search_works(query='', limit=None, offset=None, strict=False, **fields):
971  """Search for works and return a dict with a 'work-list' key.
972 
973  *Available search fields*: {fields}"""
974  return _do_mb_search('work', query, fields, limit, offset, strict)
975 
976 
977 # Lists of entities
978 @_docstring('discid')
979 def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None):
980  """Search for releases with a :musicbrainz:`Disc ID` or table of contents.
981 
982  When a `toc` is provided and no release with the disc ID is found,
983  a fuzzy search by the toc is done.
984  The `toc` should have to same format as :attr:`discid.Disc.toc_string`.
985  When a `toc` is provided, the format of the discid itself is not
986  checked server-side, so any value may be passed if searching by only
987  `toc` is desired.
988 
989  If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does,
990  the CD Stub will be returned. Prevent this from happening by
991  passing `cdstubs=False`.
992 
993  By default only results that match a format that allows discids
994  (e.g. CD) are included. To include all media formats, pass
995  `media_format='all'`.
996 
997  The result is a dict with either a 'disc' , a 'cdstub' key
998  or a 'release-list' (fuzzy match with TOC).
999  A 'disc' has an 'offset-count', an 'offset-list' and a 'release-list'.
1000  A 'cdstub' key has direct 'artist' and 'title' keys.
1001 
1002  *Available includes*: {includes}"""
1003  params = _check_filter_and_make_params("discid", includes, release_status=[],
1004  release_type=[])
1005  if toc:
1006  params["toc"] = toc
1007  if not cdstubs:
1008  params["cdstubs"] = "no"
1009  if media_format:
1010  params["media-format"] = media_format
1011  return _do_mb_query("discid", id, includes, params)
1012 
1013 @_docstring('recording')
1014 def get_recordings_by_echoprint(echoprint, includes=[], release_status=[],
1015  release_type=[]):
1016  """Search for recordings with an `echoprint <http://echoprint.me>`_.
1017  (not available on server)"""
1018  warn("Echoprints were never introduced\n"
1019  "and will not be found (404)",
1020  Warning, stacklevel=2)
1021  raise ResponseError(cause=compat.HTTPError(
1022  None, 404, "Not Found", None, None))
1023 
1024 @_docstring('recording')
1025 def get_recordings_by_puid(puid, includes=[], release_status=[],
1026  release_type=[]):
1027  """Search for recordings with a :musicbrainz:`PUID`.
1028  (not available on server)"""
1029  warn("PUID support was removed from the server\n"
1030  "and no PUIDs will be found (404)",
1031  Warning, stacklevel=2)
1032  raise ResponseError(cause=compat.HTTPError(
1033  None, 404, "Not Found", None, None))
1034 
1035 @_docstring('recording')
1036 def get_recordings_by_isrc(isrc, includes=[], release_status=[],
1037  release_type=[]):
1038  """Search for recordings with an :musicbrainz:`ISRC`.
1039  The result is a dict with an 'isrc' key,
1040  which again includes a 'recording-list'.
1041 
1042  *Available includes*: {includes}"""
1043  params = _check_filter_and_make_params("isrc", includes,
1044  release_status, release_type)
1045  return _do_mb_query("isrc", isrc, includes, params)
1046 
1047 @_docstring('work')
1048 def get_works_by_iswc(iswc, includes=[]):
1049  """Search for works with an :musicbrainz:`ISWC`.
1050  The result is a dict with a`work-list`.
1051 
1052  *Available includes*: {includes}"""
1053  return _do_mb_query("iswc", iswc, includes)
1054 
1055 
1056 def _browse_impl(entity, includes, valid_includes, limit, offset, params, release_status=[], release_type=[]):
1057  _check_includes_impl(includes, valid_includes)
1058  p = {}
1059  for k,v in params.items():
1060  if v:
1061  p[k] = v
1062  if len(p) > 1:
1063  raise Exception("Can't have more than one of " + ", ".join(params.keys()))
1064  if limit: p["limit"] = limit
1065  if offset: p["offset"] = offset
1066  filterp = _check_filter_and_make_params(entity, includes, release_status, release_type)
1067  p.update(filterp)
1068  return _do_mb_query(entity, "", includes, p)
1069 
1070 # Browse methods
1071 # Browse include are a subset of regular get includes, so we check them here
1072 # and the test in _do_mb_query will pass anyway.
1073 @_docstring('artists', browse=True)
1074 def browse_artists(recording=None, release=None, release_group=None,
1075  includes=[], limit=None, offset=None):
1076  """Get all artists linked to a recording, a release or a release group.
1077  You need to give one MusicBrainz ID.
1078 
1079  *Available includes*: {includes}"""
1080  # optional parameter work?
1081  valid_includes = VALID_BROWSE_INCLUDES['artists']
1082  params = {"recording": recording,
1083  "release": release,
1084  "release-group": release_group}
1085  return _browse_impl("artist", includes, valid_includes,
1086  limit, offset, params)
1087 
1088 @_docstring('events', browse=True)
1089 def browse_events(area=None, artist=None, place=None,
1090  includes=[], limit=None, offset=None):
1091  """Get all events linked to a area, a artist or a place.
1092  You need to give one MusicBrainz ID.
1093 
1094  *Available includes*: {includes}"""
1095  valid_includes = VALID_BROWSE_INCLUDES['events']
1096  params = {"area": area,
1097  "artist": artist,
1098  "place": place}
1099  return _browse_impl("event", includes, valid_includes,
1100  limit, offset, params)
1101 
1102 @_docstring('labels', browse=True)
1103 def browse_labels(release=None, includes=[], limit=None, offset=None):
1104  """Get all labels linked to a relase. You need to give a MusicBrainz ID.
1105 
1106  *Available includes*: {includes}"""
1107  valid_includes = VALID_BROWSE_INCLUDES['labels']
1108  params = {"release": release}
1109  return _browse_impl("label", includes, valid_includes,
1110  limit, offset, params)
1111 
1112 @_docstring('recordings', browse=True)
1113 def browse_recordings(artist=None, release=None, includes=[],
1114  limit=None, offset=None):
1115  """Get all recordings linked to an artist or a release.
1116  You need to give one MusicBrainz ID.
1117 
1118  *Available includes*: {includes}"""
1119  valid_includes = VALID_BROWSE_INCLUDES['recordings']
1120  params = {"artist": artist,
1121  "release": release}
1122  return _browse_impl("recording", includes, valid_includes,
1123  limit, offset, params)
1124 
1125 @_docstring('releases', browse=True)
1126 def browse_releases(artist=None, track_artist=None, label=None, recording=None,
1127  release_group=None, release_status=[], release_type=[],
1128  includes=[], limit=None, offset=None):
1129  """Get all releases linked to an artist, a label, a recording
1130  or a release group. You need to give one MusicBrainz ID.
1131 
1132  You can also browse by `track_artist`, which gives all releases where some
1133  tracks are attributed to that artist, but not the whole release.
1134 
1135  You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES` or
1136  :data:`musicbrainz.VALID_RELEASE_STATUSES`.
1137 
1138  *Available includes*: {includes}"""
1139  # track_artist param doesn't work yet
1140  valid_includes = VALID_BROWSE_INCLUDES['releases']
1141  params = {"artist": artist,
1142  "track_artist": track_artist,
1143  "label": label,
1144  "recording": recording,
1145  "release-group": release_group}
1146  return _browse_impl("release", includes, valid_includes, limit, offset,
1147  params, release_status, release_type)
1148 
1149 @_docstring('release-groups', browse=True)
1150 def browse_release_groups(artist=None, release=None, release_type=[],
1151  includes=[], limit=None, offset=None):
1152  """Get all release groups linked to an artist or a release.
1153  You need to give one MusicBrainz ID.
1154 
1155  You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES`.
1156 
1157  *Available includes*: {includes}"""
1158  valid_includes = VALID_BROWSE_INCLUDES['release-groups']
1159  params = {"artist": artist,
1160  "release": release}
1161  return _browse_impl("release-group", includes, valid_includes,
1162  limit, offset, params, [], release_type)
1163 
1164 @_docstring('urls', browse=True)
1165 def browse_urls(resource=None, includes=[], limit=None, offset=None):
1166  """Get urls by actual URL string.
1167  You need to give a URL string as 'resource'
1168 
1169  *Available includes*: {includes}"""
1170  # optional parameter work?
1171  valid_includes = VALID_BROWSE_INCLUDES['urls']
1172  params = {"resource": resource}
1173  return _browse_impl("url", includes, valid_includes,
1174  limit, offset, params)
1175 
1176 # browse_work is defined in the docs but has no browse criteria
1177 
1178 # Collections
1180  """List the collections for the currently :func:`authenticated <auth>` user
1181  as a dict with a 'collection-list' key."""
1182  # Missing <release-list count="n"> the count in the reply
1183  return _do_mb_query("collection", '')
1184 
1185 def get_releases_in_collection(collection, limit=None, offset=None):
1186  """List the releases in a collection.
1187  Returns a dict with a 'collection' key, which again has a 'release-list'.
1188 
1189  See `Browsing`_ for how to use `limit` and `offset`.
1190  """
1191  params = {}
1192  if limit: params["limit"] = limit
1193  if offset: params["offset"] = offset
1194  return _do_mb_query("collection", "%s/releases" % collection, [], params)
1195 
1196 
1197 # Submission methods
1198 
1199 def submit_barcodes(release_barcode):
1200  """Submits a set of {release_id1: barcode, ...}"""
1201  query = mbxml.make_barcode_request(release_barcode)
1202  return _do_mb_post("release", query)
1203 
1204 def submit_puids(recording_puids):
1205  """Submit PUIDs.
1206  (Functionality removed from server)
1207  """
1208  warn("PUID support was dropped at the server\n"
1209  "nothing will be submitted",
1210  Warning, stacklevel=2)
1211  return {'message': {'text': 'OK'}}
1212 
1213 def submit_echoprints(recording_echoprints):
1214  """Submit echoprints.
1215  (Functionality removed from server)
1216  """
1217  warn("Echoprints were never introduced\n"
1218  "nothing will be submitted",
1219  Warning, stacklevel=2)
1220  return {'message': {'text': 'OK'}}
1221 
1222 def submit_isrcs(recording_isrcs):
1223  """Submit ISRCs.
1224  Submits a set of {recording-id1: [isrc1, ...], ...}
1225  or {recording_id1: isrc, ...}.
1226  """
1227  rec2isrcs = dict()
1228  for (rec, isrcs) in recording_isrcs.items():
1229  rec2isrcs[rec] = isrcs if isinstance(isrcs, list) else [isrcs]
1230  query = mbxml.make_isrc_request(rec2isrcs)
1231  return _do_mb_post("recording", query)
1232 
1233 def submit_tags(**kwargs):
1234  """Submit user tags.
1235  Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
1236  and of the form:
1237  {entity_id1: [tag1, ...], ...}
1238 
1239  The user's tags for each entity will be set to that list, adding or
1240  removing tags as necessary. Submitting an empty list for an entity
1241  will remove all tags for that entity by the user.
1242  """
1243  query = mbxml.make_tag_request(**kwargs)
1244  return _do_mb_post("tag", query)
1245 
1246 def submit_ratings(**kwargs):
1247  """Submit user ratings.
1248  Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
1249  and of the form:
1250  {entity_id1: rating, ...}
1251 
1252  Ratings are numbers from 0-100, at intervals of 20 (20 per 'star').
1253  Submitting a rating of 0 will remove the user's rating.
1254  """
1255  query = mbxml.make_rating_request(**kwargs)
1256  return _do_mb_post("rating", query)
1257 
1258 def add_releases_to_collection(collection, releases=[]):
1259  """Add releases to a collection.
1260  Collection and releases should be identified by their MBIDs
1261  """
1262  # XXX: Maximum URI length of 16kb means we should only allow ~400 releases
1263  releaselist = ";".join(releases)
1264  return _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
1265 
1266 def remove_releases_from_collection(collection, releases=[]):
1267  """Remove releases from a collection.
1268  Collection and releases should be identified by their MBIDs
1269  """
1270  releaselist = ";".join(releases)
1271  return _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
def get_event_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:831
def submit_isrcs(recording_isrcs)
def __init__(self, method, url, data=None)
Definition: musicbrainz.py:444
def browse_urls(resource=None, includes=[], limit=None, offset=None)
def __init__(self, msg='Invalid Includes', reason=None)
Definition: musicbrainz.py:174
def browse_events(area=None, artist=None, place=None, includes=[], limit=None, offset=None)
def get_work_by_id(id, includes=[])
Definition: musicbrainz.py:880
def get_place_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:822
def set_format(fmt="xml")
Definition: musicbrainz.py:560
def get_recording_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:843
def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0)
Definition: musicbrainz.py:457
def get_instrument_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:804
def search_release_groups(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:954
def __call__(self, *args, **kwargs)
Definition: musicbrainz.py:380
def add_password(self, realm, uri, username, password)
Definition: musicbrainz.py:407
def __init__(self, message=None, cause=None)
Definition: musicbrainz.py:193
def get_url_by_id(id, includes=[])
Definition: musicbrainz.py:887
def get_artist_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:795
def submit_barcodes(release_barcode)
def search_works(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:970
def set_rate_limit(limit_or_interval=1.0, new_requests=1)
Definition: musicbrainz.py:326
def _check_filter(values, valid)
Definition: musicbrainz.py:231
def _check_includes(entity, inc)
Definition: musicbrainz.py:228
def _do_mb_search(entity, query='', fields={}, limit=None, offset=None, strict=False)
Definition: musicbrainz.py:706
def get_recordings_by_echoprint(echoprint, includes=[], release_status=[], release_type=[])
def search_labels(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:932
def _check_includes_impl(includes, valid_includes)
Definition: musicbrainz.py:223
def remove_releases_from_collection(collection, releases=[])
def search_annotations(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:897
def add_releases_to_collection(collection, releases=[])
def search_recordings(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:939
def set_parser(new_parser_fun=None)
Definition: musicbrainz.py:546
def _do_mb_post(path, body)
Definition: musicbrainz.py:774
def browse_releases(artist=None, track_artist=None, label=None, recording=None, release_group=None, release_status=[], release_type=[], includes=[], limit=None, offset=None)
def _do_mb_query(entity, id, includes=[], params={})
Definition: musicbrainz.py:684
def get_releases_in_collection(collection, limit=None, offset=None)
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None)
Definition: musicbrainz.py:979
def get_release_group_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:862
def get_area_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:786
def browse_release_groups(artist=None, release=None, release_type=[], includes=[], limit=None, offset=None)
def search_events(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:918
def search_areas(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:904
def get_release_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:853
def _browse_impl(entity, includes, valid_includes, limit, offset, params, release_status=[], release_type=[])
def set_hostname(new_hostname)
Definition: musicbrainz.py:313
def search_artists(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:911
def browse_labels(release=None, includes=[], limit=None, offset=None)
def browse_artists(recording=None, release=None, release_group=None, includes=[], limit=None, offset=None)
def _docstring(entity, browse=False)
Definition: musicbrainz.py:265
def __init__(self, msg='Invalid Includes', reason=None)
Definition: musicbrainz.py:183
def get_works_by_iswc(iswc, includes=[])
def get_series_by_id(id, includes=[])
Definition: musicbrainz.py:873
def submit_puids(recording_puids)
def submit_echoprints(recording_echoprints)
def browse_recordings(artist=None, release=None, includes=[], limit=None, offset=None)
def set_useragent(app, version, contact=None)
Definition: musicbrainz.py:300
def get_recordings_by_puid(puid, includes=[], release_status=[], release_type=[])
def _mb_request(path, method='GET', auth_required=AUTH_NO, client_required=False, args=None, data=None, body=None)
Definition: musicbrainz.py:584
def search_series(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:963
def search_instruments(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:925
def _get_auth_type(entity, id, includes)
Definition: musicbrainz.py:670
def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[])
Definition: musicbrainz.py:236
def get_label_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:813
def get_recordings_by_isrc(isrc, includes=[], release_status=[], release_type=[])
def search_releases(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:947
def submit_ratings(**kwargs)