Updated musicbrainz library

This commit is contained in:
rembo10
2015-05-19 20:26:50 -07:00
parent 885d1cc77c
commit db1519bcea
4 changed files with 396 additions and 88 deletions

View File

@@ -1 +1,2 @@
from musicbrainzngs.musicbrainz import *
from musicbrainzngs.caa import *

177
lib/musicbrainzngs/caa.py Normal file
View File

@@ -0,0 +1,177 @@
# This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Wieland Hoffmann, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
__all__ = [
'set_caa_hostname', 'get_image_list', 'get_release_group_image_list',
'get_release_group_image_front', 'get_image_front', 'get_image_back',
'get_image'
]
import json
from musicbrainzngs import compat
from musicbrainzngs import musicbrainz
hostname = "coverartarchive.org"
def set_caa_hostname(new_hostname):
"""Set the base hostname for Cover Art Archive requests.
Defaults to 'coverartarchive.org'."""
global hostname
hostname = new_hostname
def _caa_request(mbid, imageid=None, size=None, entitytype="release"):
""" Make a CAA request.
:param imageid: ``front``, ``back`` or a number from the listing obtained
with :meth:`get_image_list`.
:type imageid: str
:param size: 250, 500
:type size: str or None
:param entitytype: ``release`` or ``release-group``
:type entitytype: str
"""
# Construct the full URL for the request, including hostname and
# query string.
path = [entitytype, mbid]
if imageid and size:
path.append("%s-%s" % (imageid, size))
elif imageid:
path.append(imageid)
url = compat.urlunparse((
'http',
hostname,
'/%s' % '/'.join(path),
'',
'',
''
))
musicbrainz._log.debug("GET request for %s" % (url, ))
# Set up HTTP request handler and URL opener.
httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler]
opener = compat.build_opener(*handlers)
# Make request.
req = musicbrainz._MusicbrainzHttpRequest("GET", url, None)
# Useragent isn't needed for CAA, but we'll add it if it exists
if musicbrainz._useragent != "":
req.add_header('User-Agent', musicbrainz._useragent)
musicbrainz._log.debug("requesting with UA %s" % musicbrainz._useragent)
resp = musicbrainz._safe_read(opener, req, None)
# TODO: The content type declared by the CAA for JSON files is
# 'applicaiton/octet-stream'. This is not useful to detect whether the
# content is JSON, so default to decoding JSON if no imageid was supplied.
# http://tickets.musicbrainz.org/browse/CAA-75
if imageid:
# If we asked for an image, return the image
return resp
else:
# Otherwise it's json
return json.loads(resp)
def get_image_list(releaseid):
"""Get the list of cover art associated with a release.
The return value is the deserialized response of the `JSON listing
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease.2F.7Bmbid.7D.2F>`_
returned by the Cover Art Archive API.
If an error occurs then a musicbrainz.ResponseError will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releaseid)
def get_release_group_image_list(releasegroupid):
"""Get the list of cover art associated with a release group.
The return value is the deserialized response of the `JSON listing
<http://musicbrainz.org/doc/Cover_Art_Archive/API#.2Frelease-group.2F.7Bmbid.7D.2F>`_
returned by the Cover Art Archive API.
If an error occurs then a musicbrainz.ResponseError will
be raised with one of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
"""
return _caa_request(releasegroupid, entitytype="release-group")
def get_release_group_image_front(releasegroupid, size=None):
"""Download the front cover art for a release group.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releasegroupid, "front", size=size,
entitytype="release-group")
def get_image_front(releaseid, size=None):
"""Download the front cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "front", size=size)
def get_image_back(releaseid, size=None):
"""Download the back cover art for a release.
The `size` argument and the possible error conditions are the same as for
:meth:`get_image`.
"""
return get_image(releaseid, "back", size=size)
def get_image(mbid, coverid, size=None, entitytype="release"):
"""Download cover art for a release. The coverart file to download
is specified by the `coverid` argument.
If `size` is not specified, download the largest copy present, which can be
very large.
If an error occurs then a musicbrainz.ResponseError will be raised with one
of the following HTTP codes:
* 400: `Releaseid` is not a valid UUID or `coverid` is invalid
* 404: No release exists with an MBID of `releaseid`
* 503: Ratelimit exceeded
:param coverid: ``front``, ``back`` or a number from the listing obtained with
:meth:`get_image_list`
:type coverid: int or str
:param size: 250, 500 or None. If it is None, the largest available picture
will be downloaded. If the image originally uploaded to the
Cover Art Archive was smaller than the requested size, only
the original image will be returned.
:type size: str or None
:param entitytype: The type of entity for which to download the cover art.
This is either ``release`` or ``release-group``.
:type entitytype: str
:return: The binary image data
:type: str
"""
if isinstance(coverid, int):
coverid = "%d" % (coverid, )
if isinstance(size, int):
size = "%d" % (size, )
return _caa_request(mbid, coverid, size=size, entitytype=entitytype)

View File

@@ -36,6 +36,22 @@ NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2",
"http://musicbrainz.org/ns/ext#-2.0": "ext"}
_log = logging.getLogger("musicbrainzngs")
def get_error_message(error):
""" Given an error XML message from the webservice containing
<error><text>x</text><text>y</text></error>, return a list
of [x, y]"""
try:
tree = util.bytes_to_elementtree(error)
root = tree.getroot()
errors = []
if root.tag == "error":
for ch in root:
if ch.tag == "text":
errors.append(ch.text)
return errors
except ET.ParseError:
return None
def make_artist_credit(artists):
names = []
for artist in artists:
@@ -123,6 +139,7 @@ def parse_message(message):
"place": parse_place,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"recording": parse_recording,
"work": parse_work,
"url": parse_url,
@@ -138,6 +155,7 @@ def parse_message(message):
"place-list": parse_place_list,
"release-list": parse_release_list,
"release-group-list": parse_release_group_list,
"series-list": parse_series_list,
"recording-list": parse_recording_list,
"work-list": parse_work_list,
"url-list": parse_url_list,
@@ -297,7 +315,7 @@ def parse_relation_list(rl):
def parse_relation(relation):
result = {}
attribs = ["type", "type-id"]
elements = ["target", "direction", "begin", "end", "ended"]
elements = ["target", "direction", "begin", "end", "ended", "ordering-key"]
inner_els = {"area": parse_area,
"artist": parse_artist,
"label": parse_label,
@@ -305,6 +323,7 @@ def parse_relation(relation):
"recording": parse_recording,
"release": parse_release,
"release-group": parse_release_group,
"series": parse_series,
"attribute-list": parse_element_list,
"work": parse_work,
"target": parse_relation_target
@@ -324,6 +343,8 @@ def parse_release(release):
"label-info-list": parse_label_info_list,
"medium-list": parse_medium_list,
"release-group": parse_release_group,
"tag-list": parse_tag_list,
"user-tag-list": parse_tag_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation,
"cover-art-archive": parse_caa,
@@ -408,6 +429,22 @@ def parse_recording(recording):
return result
def parse_series_list(sl):
return [parse_series(s) for s in sl]
def parse_series(series):
result = {}
attribs = ["id", "type", "ext:score"]
elements = ["name", "disambiguation"]
inner_els = {"alias-list": parse_alias_list,
"relation-list": parse_relation_list,
"annotation": parse_annotation}
result.update(parse_attributes(attribs, series))
result.update(parse_elements(elements, inner_els, series))
return result
def parse_external_id_list(pl):
return [parse_attributes(["id"], p)["id"] for p in pl]
@@ -427,13 +464,28 @@ def parse_work(work):
"alias-list": parse_alias_list,
"iswc-list": parse_element_list,
"relation-list": parse_relation_list,
"annotation": parse_response_message}
"annotation": parse_response_message,
"attribute-list": parse_work_attribute_list
}
result.update(parse_attributes(attribs, work))
result.update(parse_elements(elements, inner_els, work))
return result
def parse_work_attribute_list(wal):
return [parse_work_attribute(wa) for wa in wal]
def parse_work_attribute(wa):
result = {}
attribs = ["type"]
result.update(parse_attributes(attribs, wa))
result["attribute"] = wa.text
return result
def parse_url_list(ul):
return [parse_url(u) for u in ul]
@@ -617,45 +669,40 @@ def make_barcode_request(release2barcode):
return ET.tostring(root, "utf-8")
def make_tag_request(artist2tags, recording2tags):
def make_tag_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS)
for rec, tags in recording2tags.items():
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS)
rec_xml.set("{%s}id" % NS, rec)
taglist = ET.SubElement(rec_xml, "{%s}user-tag-list" % NS)
for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag
art_list = ET.SubElement(root, "{%s}artist-list" % NS)
for art, tags in artist2tags.items():
art_xml = ET.SubElement(art_list, "{%s}artist" % NS)
art_xml.set("{%s}id" % NS, art)
taglist = ET.SubElement(art_xml, "{%s}user-tag-list" % NS)
for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag
for entity_type in ['artist', 'label', 'place', 'recording', 'release', 'release_group', 'work']:
entity_tags = kwargs.pop(entity_type + '_tags', None)
if entity_tags is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, tags in entity_tags.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
taglist = ET.SubElement(e_xml, "{%s}user-tag-list" % NS)
for tag in tags:
usertag_xml = ET.SubElement(taglist, "{%s}user-tag" % NS)
name_xml = ET.SubElement(usertag_xml, "{%s}name" % NS)
name_xml.text = tag
if kwargs.keys():
raise TypeError("make_tag_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")
def make_rating_request(artist2rating, recording2rating):
def make_rating_request(**kwargs):
NS = "http://musicbrainz.org/ns/mmd-2.0#"
root = ET.Element("{%s}metadata" % NS)
rec_list = ET.SubElement(root, "{%s}recording-list" % NS)
for rec, rating in recording2rating.items():
rec_xml = ET.SubElement(rec_list, "{%s}recording" % NS)
rec_xml.set("{%s}id" % NS, rec)
rating_xml = ET.SubElement(rec_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating)
art_list = ET.SubElement(root, "{%s}artist-list" % NS)
for art, rating in artist2rating.items():
art_xml = ET.SubElement(art_list, "{%s}artist" % NS)
art_xml.set("{%s}id" % NS, art)
rating_xml = ET.SubElement(art_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating)
for entity_type in ['artist', 'label', 'recording', 'release_group', 'work']:
entity_ratings = kwargs.pop(entity_type + '_ratings', None)
if entity_ratings is not None:
e_list = ET.SubElement(root, "{%s}%s-list" % (NS, entity_type.replace('_', '-')))
for e, rating in entity_ratings.items():
e_xml = ET.SubElement(e_list, "{%s}%s" % (NS, entity_type.replace('_', '-')))
e_xml.set("{%s}id" % NS, e)
rating_xml = ET.SubElement(e_xml, "{%s}user-rating" % NS)
rating_xml.text = str(rating)
if kwargs.keys():
raise TypeError("make_rating_request() got an unexpected keyword argument '%s'" % kwargs.popitem()[0])
return ET.tostring(root, "utf-8")

View File

@@ -11,25 +11,25 @@ import socket
import hashlib
import locale
import sys
import base64
import json
import xml.etree.ElementTree as etree
from xml.parsers import expat
from warnings import warn, simplefilter
from warnings import warn
from musicbrainzngs import mbxml
from musicbrainzngs import util
from musicbrainzngs import compat
import base64
_version = "0.6devMODIFIED"
_log = logging.getLogger("musicbrainzngs")
# turn on DeprecationWarnings below
simplefilter(action="once", category=DeprecationWarning)
LUCENE_SPECIAL = r'([+\-&|!(){}\[\]\^"~*?:\\\/])'
# Constants for validation.
RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'url', 'work']
RELATABLE_TYPES = ['area', 'artist', 'label', 'place', 'recording', 'release', 'release-group', 'series', 'url', 'work']
RELATION_INCLUDES = [entity + '-rels' for entity in RELATABLE_TYPES]
TAG_INCLUDES = ["tags", "user-tags"]
RATING_INCLUDES = ["ratings", "user-ratings"]
@@ -43,6 +43,9 @@ VALID_INCLUDES = {
] + RELATION_INCLUDES + TAG_INCLUDES + RATING_INCLUDES,
'annotation': [
],
'instrument': [
],
'label': [
"releases", # Subqueries
@@ -59,20 +62,23 @@ VALID_INCLUDES = {
"artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "puids", "isrcs",
"recording-level-rels", "work-level-rels", "annotation", "aliases"
] + RELATION_INCLUDES,
] + TAG_INCLUDES + RELATION_INCLUDES,
'release-group': [
"artists", "releases", "discids", "media",
"artist-credits", "annotation", "aliases"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'series': [
"annotation", "aliases"
] + RELATION_INCLUDES,
'work': [
"artists", # Subqueries
"aliases", "annotation"
] + TAG_INCLUDES + RATING_INCLUDES + RELATION_INCLUDES,
'url': RELATION_INCLUDES,
'discid': [
'discid': [ # Discid should be the same as release
"artists", "labels", "recordings", "release-groups", "media",
"artist-credits", "discids", "puids", "isrcs",
"recording-level-rels", "work-level-rels"
"recording-level-rels", "work-level-rels", "annotation", "aliases"
] + RELATION_INCLUDES,
'isrc': ["artists", "releases", "puids", "isrcs"],
'iswc': ["artists"],
@@ -93,7 +99,7 @@ VALID_RELEASE_TYPES = [
"nat",
"album", "single", "ep", "broadcast", "other", # primary types
"compilation", "soundtrack", "spokenword", "interview", "audiobook",
"live", "remix", "dj-mix", "mixtape/street", "demo", # secondary types
"live", "remix", "dj-mix", "mixtape/street", # secondary types
]
#: These can be used to filter whenever releases or release-groups are involved
VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
@@ -101,6 +107,10 @@ VALID_SEARCH_FIELDS = {
'annotation': [
'entity', 'name', 'text', 'type'
],
'area': [
'aid', 'area', 'alias', 'begin', 'comment', 'end', 'ended',
'iso', 'iso1', 'iso2', 'iso3', 'type'
],
'artist': [
'arid', 'artist', 'artistaccent', 'alias', 'begin', 'comment',
'country', 'end', 'ended', 'gender', 'ipi', 'sortname', 'tag', 'type',
@@ -133,12 +143,20 @@ VALID_SEARCH_FIELDS = {
'rgid', 'script', 'secondarytype', 'status', 'tag', 'tracks',
'tracksmedium', 'type'
],
'series': [
'alias', 'comment', 'sid', 'series', 'type'
],
'work': [
'alias', 'arid', 'artist', 'comment', 'iswc', 'lang', 'tag',
'type', 'wid', 'work', 'workaccent'
],
}
# Constants
class AUTH_YES: pass
class AUTH_NO: pass
class AUTH_IFSET: pass
# Exceptions.
@@ -280,7 +298,7 @@ def auth(u, p):
global user, password
user = u
password = p
def hpauth(u, p):
"""Set the username and password to be used in subsequent queries to
the MusicBrainz XML API that require authentication.
@@ -559,28 +577,33 @@ def set_format(fmt="xml"):
"""Sets the format that should be returned by the Web Service.
The server currently supports `xml` and `json`.
When you set the format to anything different from the default,
you need to provide your own parser with :func:`set_parser`.
This method will set a default parser for the specified format,
but you can modify it with :func:`set_parser`.
.. warning:: The json format used by the server is different from
the json format returned by the `musicbrainzngs` internal parser
when using the `xml` format!
when using the `xml` format! This format may change at any time.
"""
global ws_format
if fmt not in ["xml", "json"]:
raise ValueError("invalid format: %s" % fmt)
else:
if fmt == "xml":
ws_format = fmt
set_parser() # set to default
elif fmt == "json":
ws_format = fmt
warn("The json format is non-official and may change at any time")
set_parser(json.loads)
else:
raise ValueError("invalid format: %s" % fmt)
@_rate_limit
def _mb_request(path, method='GET', auth_required=False, client_required=False,
args=None, data=None, body=None):
def _mb_request(path, method='GET', auth_required=AUTH_NO,
client_required=False, args=None, data=None, body=None):
"""Makes a request for the specified `path` (endpoint) on /ws/2 on
the globally-specified hostname. Parses the responses and returns
the resulting object. `auth_required` and `client_required` control
whether exceptions should be raised if the client and
username/password are left unspecified, respectively.
whether exceptions should be raised if the username/password and
client are left unspecified, respectively.
"""
global parser_fun
@@ -626,11 +649,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
handlers = [httpHandler]
# Add credentials if required.
if auth_required:
add_auth = False
if auth_required == AUTH_YES:
_log.debug("Auth required for %s" % url)
if not user:
raise UsageError("authorization required; "
"use auth(user, pass) first")
add_auth = True
if auth_required == AUTH_IFSET and user:
_log.debug("Using auth for %s because user and pass is set" % url)
add_auth = True
if add_auth:
passwordMgr = _RedirectPasswordMgr()
authHandler = _DigestAuthHandler(passwordMgr)
authHandler.add_password("musicbrainz.org", (), user, password)
@@ -641,6 +672,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
# Make request.
req = _MusicbrainzHttpRequest(method, url, data)
req.add_header('User-Agent', _useragent)
# Add headphones credentials
if mb_auth:
@@ -658,16 +690,19 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
return parser_fun(resp)
def _is_auth_required(entity, includes):
""" Some calls require authentication. This returns
True if a call does, False otherwise
"""
if "user-tags" in includes or "user-ratings" in includes:
return True
elif entity.startswith("collection"):
return True
else:
return False
def _get_auth_type(entity, id, includes):
""" Some calls require authentication. This returns
True if a call does, False otherwise
"""
if "user-tags" in includes or "user-ratings" in includes:
return AUTH_YES
elif entity.startswith("collection"):
if not id:
return AUTH_YES
else:
return AUTH_IFSET
else:
return AUTH_NO
def _do_mb_query(entity, id, includes=[], params={}):
"""Make a single GET call to the MusicBrainz XML API. `entity` is a
@@ -681,7 +716,7 @@ def _do_mb_query(entity, id, includes=[], params={}):
if not isinstance(includes, list):
includes = [includes]
_check_includes(entity, includes)
auth_required = _is_auth_required(entity, includes)
auth_required = _get_auth_type(entity, id, includes)
args = dict(params)
if len(includes) > 0:
inc = " ".join(includes)
@@ -704,8 +739,8 @@ def _do_mb_search(entity, query='', fields={},
if query:
clean_query = util._unicode(query)
if fields:
clean_query = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])',
r'\\\1', clean_query)
clean_query = re.sub(LUCENE_SPECIAL, r'\\\1',
clean_query)
if strict:
query_parts.append('"%s"' % clean_query)
else:
@@ -721,11 +756,11 @@ def _do_mb_search(entity, query='', fields={},
elif key == "puid":
warn("PUID support was removed from server\n"
"the 'puid' field is ignored",
DeprecationWarning, stacklevel=2)
Warning, stacklevel=2)
# Escape Lucene's special characters.
value = util._unicode(value)
value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\\/])', r'\\\1', value)
value = re.sub(LUCENE_SPECIAL, r'\\\1', value)
if value:
if strict:
query_parts.append('%s:"%s"' % (key, value))
@@ -752,18 +787,18 @@ def _do_mb_search(entity, query='', fields={},
def _do_mb_delete(path):
"""Send a DELETE request for the specified object.
"""
return _mb_request(path, 'DELETE', True, True)
return _mb_request(path, 'DELETE', AUTH_YES, True)
def _do_mb_put(path):
"""Send a PUT request for the specified object.
"""
return _mb_request(path, 'PUT', True, True)
return _mb_request(path, 'PUT', AUTH_YES, True)
def _do_mb_post(path, body):
"""Perform a single POST call for an endpoint with a specified
request body.
"""
return _mb_request(path, 'POST', True, True, body=body)
return _mb_request(path, 'POST', AUTH_YES, True, body=body)
# The main interface!
@@ -788,6 +823,15 @@ def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
release_status, release_type)
return _do_mb_query("artist", id, includes, params)
@_docstring('instrument')
def get_instrument_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the instrument with the MusicBrainz `id` as a dict with an 'artist' key.
*Available includes*: {includes}"""
params = _check_filter_and_make_params("instrument", includes,
release_status, release_type)
return _do_mb_query("instrument", id, includes, params)
@_docstring('label')
def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
"""Get the label with the MusicBrainz `id` as a dict with a 'label' key.
@@ -836,6 +880,13 @@ def get_release_group_by_id(id, includes=[],
release_status, release_type)
return _do_mb_query("release-group", id, includes, params)
@_docstring('series')
def get_series_by_id(id, includes=[]):
"""Get the series with the MusicBrainz `id` as a dict with a 'series' key.
*Available includes*: {includes}"""
return _do_mb_query("series", id, includes)
@_docstring('work')
def get_work_by_id(id, includes=[]):
"""Get the work with the MusicBrainz `id` as a dict with a 'work' key.
@@ -860,6 +911,13 @@ def search_annotations(query='', limit=None, offset=None, strict=False, **fields
*Available search fields*: {fields}"""
return _do_mb_search('annotation', query, fields, limit, offset, strict)
@_docstring('area')
def search_areas(query='', limit=None, offset=None, strict=False, **fields):
"""Search for areas and return a dict with an 'area-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('area', query, fields, limit, offset, strict)
@_docstring('artist')
def search_artists(query='', limit=None, offset=None, strict=False, **fields):
"""Search for artists and return a dict with an 'artist-list' key.
@@ -898,6 +956,13 @@ def search_release_groups(query='', limit=None, offset=None,
*Available search fields*: {fields}"""
return _do_mb_search('release-group', query, fields, limit, offset, strict)
@_docstring('series')
def search_series(query='', limit=None, offset=None, strict=False, **fields):
"""Search for series and return a dict with a 'series-list' key.
*Available search fields*: {fields}"""
return _do_mb_search('series', query, fields, limit, offset, strict)
@_docstring('work')
def search_works(query='', limit=None, offset=None, strict=False, **fields):
"""Search for works and return a dict with a 'work-list' key.
@@ -907,18 +972,25 @@ def search_works(query='', limit=None, offset=None, strict=False, **fields):
# Lists of entities
@_docstring('release')
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True):
"""Search for releases with a :musicbrainz:`Disc ID`.
@_docstring('discid')
def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True, media_format=None):
"""Search for releases with a :musicbrainz:`Disc ID` or table of contents.
When a `toc` is provided and no release with the disc ID is found,
a fuzzy search by the toc is done.
The `toc` should have to same format as :attr:`discid.Disc.toc_string`.
When a `toc` is provided, the format of the discid itself is not
checked server-side, so any value may be passed if searching by only
`toc` is desired.
If no toc matches in musicbrainz but a :musicbrainz:`CD Stub` does,
the CD Stub will be returned. Prevent this from happening by
passing `cdstubs=False`.
By default only results that match a format that allows discids
(e.g. CD) are included. To include all media formats, pass
`media_format='all'`.
The result is a dict with either a 'disc' , a 'cdstub' key
or a 'release-list' (fuzzy match with TOC).
A 'disc' has a 'release-list' and a 'cdstub' key has direct 'artist'
@@ -931,6 +1003,8 @@ def get_releases_by_discid(id, includes=[], toc=None, cdstubs=True):
params["toc"] = toc
if not cdstubs:
params["cdstubs"] = "no"
if media_format:
params["media-format"] = media_format
return _do_mb_query("discid", id, includes, params)
@_docstring('recording')
@@ -940,7 +1014,7 @@ def get_recordings_by_echoprint(echoprint, includes=[], release_status=[],
(not available on server)"""
warn("Echoprints were never introduced\n"
"and will not be found (404)",
DeprecationWarning, stacklevel=2)
Warning, stacklevel=2)
raise ResponseError(cause=compat.HTTPError(
None, 404, "Not Found", None, None))
@@ -951,7 +1025,7 @@ def get_recordings_by_puid(puid, includes=[], release_status=[],
(not available on server)"""
warn("PUID support was removed from the server\n"
"and no PUIDs will be found (404)",
DeprecationWarning, stacklevel=2)
Warning, stacklevel=2)
raise ResponseError(cause=compat.HTTPError(
None, 404, "Not Found", None, None))
@@ -1116,7 +1190,7 @@ def submit_puids(recording_puids):
"""
warn("PUID support was dropped at the server\n"
"nothing will be submitted",
DeprecationWarning, stacklevel=2)
Warning, stacklevel=2)
return {'message': {'text': 'OK'}}
def submit_echoprints(recording_echoprints):
@@ -1125,7 +1199,7 @@ def submit_echoprints(recording_echoprints):
"""
warn("Echoprints were never introduced\n"
"nothing will be submitted",
DeprecationWarning, stacklevel=2)
Warning, stacklevel=2)
return {'message': {'text': 'OK'}}
def submit_isrcs(recording_isrcs):
@@ -1139,20 +1213,29 @@ def submit_isrcs(recording_isrcs):
query = mbxml.make_isrc_request(rec2isrcs)
return _do_mb_post("recording", query)
def submit_tags(artist_tags={}, recording_tags={}):
def submit_tags(**kwargs):
"""Submit user tags.
Artist or recording parameters are of the form:
Takes parameters named e.g. 'artist_tags', 'recording_tags', etc.,
and of the form:
{entity_id1: [tag1, ...], ...}
The user's tags for each entity will be set to that list, adding or
removing tags as necessary. Submitting an empty list for an entity
will remove all tags for that entity by the user.
"""
query = mbxml.make_tag_request(artist_tags, recording_tags)
query = mbxml.make_tag_request(**kwargs)
return _do_mb_post("tag", query)
def submit_ratings(artist_ratings={}, recording_ratings={}):
""" Submit user ratings.
Artist or recording parameters are of the form:
def submit_ratings(**kwargs):
"""Submit user ratings.
Takes parameters named e.g. 'artist_ratings', 'recording_ratings', etc.,
and of the form:
{entity_id1: rating, ...}
Ratings are numbers from 0-100, at intervals of 20 (20 per 'star').
Submitting a rating of 0 will remove the user's rating.
"""
query = mbxml.make_rating_request(artist_ratings, recording_ratings)
query = mbxml.make_rating_request(**kwargs)
return _do_mb_post("rating", query)
def add_releases_to_collection(collection, releases=[]):