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
6import re
7import threading
8import time
9import logging
10import socket
11import hashlib
12import locale
13import sys
14import json
15import xml.etree.ElementTree as etree
16from xml.parsers import expat
17from warnings import warn
18
19from musicbrainzngs import mbxml
20from musicbrainzngs import util
21from musicbrainzngs import compat
22
23_version = "0.6dev"
24_log = logging.getLogger("musicbrainzngs")
25
26LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
27
28# Constants for validation.
29
30RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'event', 'recording', 'release', 'release-group', 'series', 'url', 'work', 'instrument']
31RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES]
32TAG_INCLUDES = ["tags", "user-tags"]
33RATING_INCLUDES = ["ratings", "user-ratings"]
34
35VALID_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}
85VALID_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
97VALID_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
104VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
105VALID_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
155class AUTH_YES: pass
156class AUTH_NO: pass
157class AUTH_IFSET: pass
158
159
160# Exceptions.
161
162class 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
212class 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
223def _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)
228def _check_includes(entity, inc):
229 _check_includes_impl(inc, VALID_INCLUDES[entity])
230
231def _check_filter(values, valid):
232 for v in values:
233 if v not in valid:
234 raise InvalidFilterError(v)
235
236def _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
265def _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
287user = password = ""
288hostname = "musicbrainz.org"
289_client = ""
290_useragent = ""
291
292def 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
300def 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
313def 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
322limit_interval = 1.0
323limit_requests = 1
324do_rate_limit = True
325
326def 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
348class _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
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)
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
396class _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
411class _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
442class _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
457def _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.
517if hasattr(etree, 'ParseError'):
518 ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
519else:
520 ETREE_EXCEPTIONS = (expat.ExpatError)
521
522
523# Parsing setup
524
526 """Return the raw response (XML)"""
527 return resp
528
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
543parser_fun = mb_parser_xml
544ws_format = "xml"
545
546def 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
560def 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
584def _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
670def _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
684def _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
706def _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
765 """Send a DELETE request for the specified object.
766 """
767 return _mb_request(path, 'DELETE', AUTH_YES, True)
768
769def _do_mb_put(path):
770 """Send a PUT request for the specified object.
771 """
772 return _mb_request(path, 'PUT', AUTH_YES, True)
773
774def _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')
786def 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')
795def 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')
804def 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')
813def 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')
822def 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')
831def 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')
843def 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')
853def 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')
862def 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')
873def 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')
880def 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')
887def 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')
897def 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')
904def 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')
911def 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')
918def 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')
925def 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')
932def 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')
939def 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')
947def 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')
954def 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')
963def 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')
970def 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')
979def 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')
1014def 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')
1025def 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')
1036def 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')
1048def 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
1056def _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)
1074def 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)
1089def 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)
1103def 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)
1113def 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)
1126def 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)
1150def 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)
1165def 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
1185def 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
1199def 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
1204def 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
1213def 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
1222def 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
1233def 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
1246def 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
1258def 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
1266def 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 __init__(self, msg='Invalid Includes', reason=None)
Definition: musicbrainz.py:183
def __init__(self, msg='Invalid Includes', reason=None)
Definition: musicbrainz.py:174
def __init__(self, message=None, cause=None)
Definition: musicbrainz.py:193
def __init__(self, method, url, data=None)
Definition: musicbrainz.py:444
def add_password(self, realm, uri, username, password)
Definition: musicbrainz.py:407
def __call__(self, *args, **kwargs)
Definition: musicbrainz.py:380
def _safe_read(opener, req, body=None, max_retries=8, retry_delay_delta=2.0)
Definition: musicbrainz.py:457
def submit_puids(recording_puids)
def _browse_impl(entity, includes, valid_includes, limit, offset, params, release_status=[], release_type=[])
def search_release_groups(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:955
def browse_labels(release=None, includes=[], limit=None, offset=None)
def search_artists(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:911
def submit_isrcs(recording_isrcs)
def search_events(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:918
def search_instruments(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:925
def _do_mb_search(entity, query='', fields={}, limit=None, offset=None, strict=False)
Definition: musicbrainz.py:707
def add_releases_to_collection(collection, releases=[])
def submit_ratings(**kwargs)
def search_areas(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:904
def search_recordings(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:940
def _check_filter(values, valid)
Definition: musicbrainz.py:231
def submit_echoprints(recording_echoprints)
def _get_auth_type(entity, id, includes)
Definition: musicbrainz.py:670
def _mb_request(path, method='GET', auth_required=AUTH_NO, client_required=False, args=None, data=None, body=None)
Definition: musicbrainz.py:585
def set_format(fmt="xml")
Definition: musicbrainz.py:560
def browse_events(area=None, artist=None, place=None, includes=[], limit=None, offset=None)
def get_releases_in_collection(collection, limit=None, offset=None)
def set_useragent(app, version, contact=None)
Definition: musicbrainz.py:300
def _docstring(entity, browse=False)
Definition: musicbrainz.py:265
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None)
Definition: musicbrainz.py:979
def get_event_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:831
def get_instrument_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:804
def get_works_by_iswc(iswc, includes=[])
def search_releases(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:947
def get_recordings_by_isrc(isrc, includes=[], release_status=[], release_type=[])
def get_artist_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:795
def remove_releases_from_collection(collection, releases=[])
def get_recording_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:843
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 set_parser(new_parser_fun=None)
Definition: musicbrainz.py:546
def search_works(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:970
def get_work_by_id(id, includes=[])
Definition: musicbrainz.py:880
def get_url_by_id(id, includes=[])
Definition: musicbrainz.py:887
def browse_release_groups(artist=None, release=None, release_type=[], includes=[], limit=None, offset=None)
def get_recordings_by_puid(puid, includes=[], release_status=[], release_type=[])
def get_label_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:813
def get_area_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:786
def search_series(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:963
def _do_mb_query(entity, id, includes=[], params={})
Definition: musicbrainz.py:684
def _do_mb_post(path, body)
Definition: musicbrainz.py:774
def browse_recordings(artist=None, release=None, includes=[], limit=None, offset=None)
def _check_includes(entity, inc)
Definition: musicbrainz.py:228
def get_release_group_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:863
def get_recordings_by_echoprint(echoprint, includes=[], release_status=[], release_type=[])
def get_series_by_id(id, includes=[])
Definition: musicbrainz.py:873
def set_rate_limit(limit_or_interval=1.0, new_requests=1)
Definition: musicbrainz.py:326
def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[])
Definition: musicbrainz.py:236
def get_release_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:853
def browse_urls(resource=None, includes=[], limit=None, offset=None)
def submit_barcodes(release_barcode)
def set_hostname(new_hostname)
Definition: musicbrainz.py:313
def _check_includes_impl(includes, valid_includes)
Definition: musicbrainz.py:223
def get_place_by_id(id, includes=[], release_status=[], release_type=[])
Definition: musicbrainz.py:822
def browse_artists(recording=None, release=None, release_group=None, includes=[], limit=None, offset=None)
def search_annotations(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:897
def search_labels(query='', limit=None, offset=None, strict=False, **fields)
Definition: musicbrainz.py:932