15 import xml.etree.ElementTree
as etree
16 from xml.parsers
import expat
17 from warnings
import warn
19 from musicbrainzngs
import mbxml
20 from musicbrainzngs
import util
21 from musicbrainzngs
import compat
24 _log = logging.getLogger(
"musicbrainzngs")
26 LUCENE_SPECIAL =
r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
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"]
36 'area' : [
"aliases",
"annotation"] + RELATION_INCLUDES,
38 "recordings",
"releases",
"release-groups",
"works",
39 "various-artists",
"discids",
"media",
"isrcs",
40 "aliases",
"annotation"
41 ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
45 'instrument': [
"aliases",
"annotation"
46 ] + RELATION_INCLUDES + TAG_INCLUDES,
50 "aliases",
"annotation"
51 ] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
52 'place' : [
"aliases",
"annotation"] + RELATION_INCLUDES + TAG_INCLUDES,
53 'event' : [
"aliases"] + RELATION_INCLUDES,
55 "artists",
"releases",
56 "discids",
"media",
"artist-credits",
"isrcs",
57 "annotation",
"aliases"
58 ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
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,
65 "artists",
"releases",
"discids",
"media",
66 "artist-credits",
"annotation",
"aliases"
67 ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
69 "annotation",
"aliases"
70 ] + RELATION_INCLUDES,
73 "aliases",
"annotation"
74 ] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
75 'url': RELATION_INCLUDES,
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"],
83 'collection': [
'releases'],
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
97 VALID_RELEASE_TYPES = [
99 "album",
"single",
"ep",
"broadcast",
"other",
100 "compilation",
"soundtrack",
"spokenword",
"interview",
"audiobook",
101 "live",
"remix",
"dj-mix",
"mixtape/street",
104 VALID_RELEASE_STATUSES = [
"official",
"promotion",
"bootleg",
"pseudo-release"]
105 VALID_SEARCH_FIELDS = {
107 'entity',
'name',
'text',
'type'
110 'aid',
'area',
'alias',
'begin',
'comment',
'end',
'ended',
111 'iso',
'iso1',
'iso2',
'iso3',
'type'
114 'arid',
'artist',
'artistaccent',
'alias',
'begin',
'comment',
115 'country',
'end',
'ended',
'gender',
'ipi',
'sortname',
'tag',
'type',
116 'area',
'beginarea',
'endarea'
119 'alias',
'begin',
'code',
'comment',
'country',
'end',
'ended',
120 'ipi',
'label',
'labelaccent',
'laid',
'sortname',
'type',
'tag',
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'
132 'arid',
'artist',
'artistname',
'comment',
'creditname',
133 'primarytype',
'rgid',
'releasegroup',
'releasegroupaccent',
134 'releases',
'release',
'reid',
'secondarytype',
'status',
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'
146 'alias',
'comment',
'sid',
'series',
'type'
149 'alias',
'arid',
'artist',
'comment',
'iswc',
'lang',
'tag',
150 'type',
'wid',
'work',
'workaccent'
163 """Base class for all exceptions related to MusicBrainz."""
167 """Error related to misuse of the module API."""
174 def __init__(self, msg='Invalid Includes', reason=None):
175 super(InvalidIncludeError, self).
__init__(self)
183 def __init__(self, msg='Invalid Includes', reason=None):
184 super(InvalidFilterError, self).
__init__(self)
192 """Error related to MusicBrainz API requests."""
194 """Pass ``cause`` if this exception was caused by another
205 msg +=
"caused by: %s" % str(self.
cause)
209 """Problem communicating with the MB server."""
212 class ResponseError(WebServiceError):
213 """Bad response sent by the MB server."""
217 """Received a HTTP 401 response while accessing a protected resource."""
225 if i
not in valid_includes:
227 "%s is not a valid include" % i)
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.
241 if isinstance(release_status, compat.basestring):
242 release_status = [release_status]
243 if isinstance(release_type, compat.basestring):
244 release_type = [release_type]
249 and "releases" not in includes
and entity !=
"release"):
252 and "release-groups" not in includes
and "releases" not in includes
253 and entity
not in [
"release-group",
"release"]):
255 "with no releases or release-groups involved")
259 if len(release_status):
260 params[
"status"] =
"|".join(release_status)
261 if len(release_type):
262 params[
"type"] =
"|".join(release_type)
266 def _decorator(func):
268 includes = list(VALID_BROWSE_INCLUDES.get(entity, []))
270 includes = list(VALID_INCLUDES.get(entity, []))
272 if "puids" in includes: includes.remove(
"puids")
273 includes =
", ".join(includes)
275 search_fields = list(VALID_SEARCH_FIELDS.get(entity, []))
277 if "puid" in search_fields: search_fields.remove(
"puid")
278 func.__doc__ = func.__doc__.format(includes=includes,
279 fields=
", ".join(search_fields))
288 hostname =
"musicbrainz.org"
293 """Set the username and password to be used in subsequent queries to
294 the MusicBrainz XML API that require authentication.
296 global user, password
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)
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)
314 """Set the hostname for MusicBrainz webservice requests.
315 Defaults to 'musicbrainz.org'.
316 You can also include a port: 'localhost:8000'."""
318 hostname = new_hostname
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`).
334 global limit_interval
335 global limit_requests
337 if isinstance(limit_or_interval, bool):
338 do_rate_limit = limit_or_interval
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")
345 limit_interval = limit_or_interval
346 limit_requests = new_requests
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.
363 """Update remaining requests based on the elapsed time since
364 they were last calculated.
372 since_last_call = time.time() - self.
last_call
374 (limit_requests / limit_interval)
376 float(limit_requests))
388 (limit_requests / limit_interval))
393 return self.
fun(*args, **kwargs)
409 self.
_realms[realm] = (username, password)
413 qop = chal.get (
'qop',
None)
414 if qop
and ',' in qop
and 'auth' in qop.split (
','):
417 return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal)
420 """The MusicBrainz server also accepts UTF-8 encoded passwords."""
421 encoding = sys.stdin.encoding
or locale.getpreferredencoding()
424 msg = msg.decode(encoding)
425 except AttributeError:
428 return msg.encode(
"utf-8")
432 algorithm = algorithm.upper()
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()
439 KD =
lambda s, d: H(
"%s:%s" % (s, d))
443 """ A custom request handler that allows DELETE and PUT"""
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)
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.
464 for retry_num
in range(max_retries):
466 _log.info(
"retrying after delay (#%i)" % retry_num)
467 time.sleep(retry_num * retry_delay_delta)
471 f = opener.open(req, body)
476 except compat.HTTPError
as exc:
477 if exc.code
in (400, 404, 411):
480 elif exc.code
in (503, 502, 500):
482 _log.info(
"HTTP error %i" % exc.code)
483 elif exc.code
in (401, ):
488 _log.info(
"unknown HTTP error %i" % exc.code)
490 except compat.BadStatusLine
as exc:
491 _log.info(
"bad status line")
493 except compat.HTTPException
as exc:
494 _log.info(
"miscellaneous HTTP exception: %s" % str(exc))
496 except compat.URLError
as exc:
497 if isinstance(exc.reason, socket.error):
498 code = exc.reason.errno
502 except socket.timeout
as exc:
503 _log.info(
"socket timeout")
505 except socket.error
as exc:
509 except IOError
as exc:
513 raise NetworkError(
"retried %i times" % max_retries, last_exc)
517 if hasattr(etree,
'ParseError'):
518 ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError)
520 ETREE_EXCEPTIONS = (expat.ExpatError)
526 """Return the raw response (XML)"""
530 """Return a Python dict representing the XML response"""
533 return mbxml.parse_message(resp)
534 except UnicodeError
as exc:
536 except Exception
as exc:
537 if isinstance(exc, ETREE_EXCEPTIONS):
543 parser_fun = mb_parser_xml
547 """Sets the function used to parse the response from the
548 MusicBrainz web service.
550 If no parser is given, the parser is reset to the default parser
551 :func:`mb_parser_xml`.
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
561 """Sets the format that should be returned by the Web Service.
562 The server currently supports `xml` and `json`.
564 This method will set a default parser for the specified format,
565 but you can modify it with :func:`set_parser`.
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.
577 warn(
"The json format is non-official and may change at any time")
580 raise ValueError(
"invalid format: %s" % fmt)
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.
597 args =
dict(args)
or {}
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)\")")
604 args[
"client"] = _client
606 if ws_format !=
"xml":
607 args[
"fmt"] = ws_format
614 for key, value
in sorted(args.items()):
615 if isinstance(value, compat.unicode):
616 value = value.encode(
'utf8')
617 newargs.append((key, value))
621 url = compat.urlunparse((
626 compat.urlencode(newargs),
629 _log.debug(
"%s request for %s" % (method, url))
632 httpHandler = compat.HTTPHandler(debuglevel=0)
633 handlers = [httpHandler]
637 if auth_required == AUTH_YES:
638 _log.debug(
"Auth required for %s" % url)
641 "use auth(user, pass) first")
644 if auth_required == AUTH_IFSET
and user:
645 _log.debug(
"Using auth for %s because user and pass is set" % url)
651 authHandler.add_password(
"musicbrainz.org", (), user, password)
652 handlers.append(authHandler)
654 opener = compat.build_opener(*handlers)
658 req.add_header(
'User-Agent', _useragent)
659 _log.debug(
"requesting with UA %s" % _useragent)
661 req.add_header(
'Content-Type',
'application/xml; charset=UTF-8')
662 elif not data
and not req.has_header(
'Content-Length'):
665 req.add_header(
'Content-Length',
'0')
671 """ Some calls require authentication. This returns
672 True if a call does, False otherwise
674 if "user-tags" in includes
or "user-ratings" in includes:
676 elif entity.startswith(
"collection"):
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.
693 if not isinstance(includes, list):
694 includes = [includes]
698 if len(includes) > 0:
699 inc =
" ".join(includes)
703 path =
'%s/%s' % (entity, id)
704 return _mb_request(path,
'GET', auth_required, args=args)
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.
717 clean_query = util._unicode(query)
719 clean_query = re.sub(LUCENE_SPECIAL,
r'\\\1',
722 query_parts.append(
'"%s"' % clean_query)
724 query_parts.append(clean_query.lower())
726 query_parts.append(clean_query)
727 for key, value
in fields.items():
729 if key
not in VALID_SEARCH_FIELDS[entity]:
731 '%s is not a valid search field for %s' % (key, entity)
734 warn(
"PUID support was removed from server\n"
735 "the 'puid' field is ignored",
736 Warning, stacklevel=2)
739 value = util._unicode(value)
740 value = re.sub(LUCENE_SPECIAL,
r'\\\1', value)
743 query_parts.append(
'%s:"%s"' % (key, value))
745 value = value.lower()
746 query_parts.append(
'%s:(%s)' % (key, value))
748 full_query =
' AND '.join(query_parts).strip()
750 full_query =
' '.join(query_parts).strip()
753 raise ValueError(
'at least one query term is required')
756 params = {
'query': full_query}
758 params[
'limit'] = str(limit)
760 params[
'offset'] = str(offset)
765 """Send a DELETE request for the specified object.
770 """Send a PUT request for the specified object.
775 """Perform a single POST call for an endpoint with a specified
778 return _mb_request(path,
'POST', AUTH_YES,
True, body=body)
787 """Get the area with the MusicBrainz `id` as a dict with an 'area' key.
789 *Available includes*: {includes}"""
791 release_status, release_type)
796 """Get the artist with the MusicBrainz `id` as a dict with an 'artist' key.
798 *Available includes*: {includes}"""
800 release_status, release_type)
805 """Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key.
807 *Available includes*: {includes}"""
809 release_status, release_type)
814 """Get the label with the MusicBrainz `id` as a dict with a 'label' key.
816 *Available includes*: {includes}"""
818 release_status, release_type)
823 """Get the place with the MusicBrainz `id` as a dict with an 'place' key.
825 *Available includes*: {includes}"""
827 release_status, release_type)
832 """Get the event with the MusicBrainz `id` as a dict with an 'event' key.
834 The event dict has the following keys:
835 `id`, `type`, `name`, `time`, `disambiguation` and `life-span`.
837 *Available includes*: {includes}"""
839 release_status, release_type)
844 """Get the recording with the MusicBrainz `id` as a dict
845 with a 'recording' key.
847 *Available includes*: {includes}"""
849 release_status, release_type)
854 """Get the release with the MusicBrainz `id` as a dict with a 'release' key.
856 *Available includes*: {includes}"""
858 release_status, release_type)
863 release_status=[], release_type=[]):
864 """Get the release group with the MusicBrainz `id` as a dict
865 with a 'release-group' key.
867 *Available includes*: {includes}"""
869 release_status, release_type)
870 return _do_mb_query(
"release-group", id, includes, params)
874 """Get the series with the MusicBrainz `id` as a dict with a 'series' key.
876 *Available includes*: {includes}"""
881 """Get the work with the MusicBrainz `id` as a dict with a 'work' key.
883 *Available includes*: {includes}"""
888 """Get the url with the MusicBrainz `id` as a dict with a 'url' key.
890 *Available includes*: {includes}"""
898 """Search for annotations and return a dict with an 'annotation-list' key.
900 *Available search fields*: {fields}"""
901 return _do_mb_search(
'annotation', query, fields, limit, offset, strict)
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.
907 *Available search fields*: {fields}"""
908 return _do_mb_search(
'area', query, fields, limit, offset, strict)
912 """Search for artists and return a dict with an 'artist-list' key.
914 *Available search fields*: {fields}"""
915 return _do_mb_search(
'artist', query, fields, limit, offset, strict)
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.
921 *Available search fields*: {fields}"""
922 return _do_mb_search(
'event', query, fields, limit, offset, strict)
926 """Search for instruments and return a dict with a 'instrument-list' key.
928 *Available search fields*: {fields}"""
929 return _do_mb_search(
'instrument', query, fields, limit, offset, strict)
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.
935 *Available search fields*: {fields}"""
936 return _do_mb_search(
'label', query, fields, limit, offset, strict)
940 strict=False, **fields):
941 """Search for recordings and return a dict with a 'recording-list' key.
943 *Available search fields*: {fields}"""
944 return _do_mb_search(
'recording', query, fields, limit, offset, strict)
948 """Search for recordings and return a dict with a 'recording-list' key.
950 *Available search fields*: {fields}"""
951 return _do_mb_search(
'release', query, fields, limit, offset, strict)
955 strict=False, **fields):
956 """Search for release groups and return a dict
957 with a 'release-group-list' key.
959 *Available search fields*: {fields}"""
960 return _do_mb_search(
'release-group', query, fields, limit, offset, strict)
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.
966 *Available search fields*: {fields}"""
967 return _do_mb_search(
'series', query, fields, limit, offset, strict)
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.
973 *Available search fields*: {fields}"""
974 return _do_mb_search(
'work', query, fields, limit, offset, strict)
980 """Search for releases with a :musicbrainz:`Disc ID` or table of contents.
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
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`.
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'`.
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.
1002 *Available includes*: {includes}"""
1008 params[
"cdstubs"] =
"no"
1010 params[
"media-format"] = media_format
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)
1022 None, 404,
"Not Found",
None,
None))
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)
1033 None, 404,
"Not Found",
None,
None))
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'.
1042 *Available includes*: {includes}"""
1044 release_status, release_type)
1049 """Search for works with an :musicbrainz:`ISWC`.
1050 The result is a dict with a`work-list`.
1052 *Available includes*: {includes}"""
1056 def _browse_impl(entity, includes, valid_includes, limit, offset, params, release_status=[], release_type=[]):
1059 for k,v
in params.items():
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
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.
1079 *Available includes*: {includes}"""
1081 valid_includes = VALID_BROWSE_INCLUDES[
'artists']
1082 params = {
"recording": recording,
1084 "release-group": release_group}
1085 return _browse_impl(
"artist", includes, valid_includes,
1086 limit, offset, params)
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.
1094 *Available includes*: {includes}"""
1095 valid_includes = VALID_BROWSE_INCLUDES[
'events']
1096 params = {
"area": area,
1100 limit, offset, params)
1104 """Get all labels linked to a relase. You need to give a MusicBrainz ID.
1106 *Available includes*: {includes}"""
1107 valid_includes = VALID_BROWSE_INCLUDES[
'labels']
1108 params = {
"release": release}
1110 limit, offset, params)
1114 limit=None, offset=None):
1115 """Get all recordings linked to an artist or a release.
1116 You need to give one MusicBrainz ID.
1118 *Available includes*: {includes}"""
1119 valid_includes = VALID_BROWSE_INCLUDES[
'recordings']
1120 params = {
"artist": artist,
1122 return _browse_impl(
"recording", includes, valid_includes,
1123 limit, offset, params)
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.
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.
1135 You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES` or
1136 :data:`musicbrainz.VALID_RELEASE_STATUSES`.
1138 *Available includes*: {includes}"""
1140 valid_includes = VALID_BROWSE_INCLUDES[
'releases']
1141 params = {
"artist": artist,
1142 "track_artist": track_artist,
1144 "recording": recording,
1145 "release-group": release_group}
1146 return _browse_impl(
"release", includes, valid_includes, limit, offset,
1147 params, release_status, 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.
1155 You can filter by :data:`musicbrainz.VALID_RELEASE_TYPES`.
1157 *Available includes*: {includes}"""
1158 valid_includes = VALID_BROWSE_INCLUDES[
'release-groups']
1159 params = {
"artist": artist,
1161 return _browse_impl(
"release-group", includes, valid_includes,
1162 limit, offset, params, [], release_type)
1166 """Get urls by actual URL string.
1167 You need to give a URL string as 'resource'
1169 *Available includes*: {includes}"""
1171 valid_includes = VALID_BROWSE_INCLUDES[
'urls']
1172 params = {
"resource": resource}
1174 limit, offset, params)
1180 """List the collections for the currently :func:`authenticated <auth>` user
1181 as a dict with a 'collection-list' key."""
1186 """List the releases in a collection.
1187 Returns a dict with a 'collection' key, which again has a 'release-list'.
1189 See `Browsing`_ for how to use `limit` and `offset`.
1192 if limit: params[
"limit"] = limit
1193 if offset: params[
"offset"] = offset
1194 return _do_mb_query(
"collection",
"%s/releases" % collection, [], params)
1200 """Submits a set of {release_id1: barcode, ...}"""
1201 query = mbxml.make_barcode_request(release_barcode)
1206 (Functionality removed from server)
1208 warn(
"PUID support was dropped at the server\n"
1209 "nothing will be submitted",
1210 Warning, stacklevel=2)
1211 return {
'message': {
'text':
'OK'}}
1214 """Submit echoprints.
1215 (Functionality removed from server)
1217 warn(
"Echoprints were never introduced\n"
1218 "nothing will be submitted",
1219 Warning, stacklevel=2)
1220 return {
'message': {
'text':
'OK'}}
1224 Submits a set of {recording-id1: [isrc1, ...], ...}
1225 or {recording_id1: isrc, ...}.
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)
1234 """Submit user tags.
1235 Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
1237 {entity_id1: [tag1, ...], ...}
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.
1243 query = mbxml.make_tag_request(**kwargs)
1247 """Submit user ratings.
1248 Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
1250 {entity_id1: rating, ...}
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.
1255 query = mbxml.make_rating_request(**kwargs)
1259 """Add releases to a collection.
1260 Collection and releases should be identified by their MBIDs
1263 releaselist =
";".join(releases)
1264 return _do_mb_put(
"collection/%s/releases/%s" % (collection, releaselist))
1267 """Remove releases from a collection.
1268 Collection and releases should be identified by their MBIDs
1270 releaselist =
";".join(releases)
1271 return _do_mb_delete(
"collection/%s/releases/%s" % (collection, releaselist))