mirror of
https://github.com/rembo10/headphones.git
synced 2026-05-06 03:39:51 +01:00
Updated musicbrainz library
This commit is contained in:
@@ -1 +1,2 @@
|
||||
from musicbrainzngs.musicbrainz import *
|
||||
from musicbrainzngs.caa import *
|
||||
|
||||
177
lib/musicbrainzngs/caa.py
Normal file
177
lib/musicbrainzngs/caa.py
Normal 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)
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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=[]):
|
||||
|
||||
Reference in New Issue
Block a user