Changed findArtist to use the musicbrainzngs library, the output is identical.

(old musicbrainz 2 library is deprecated and broken)
This commit is contained in:
Patrick Speiser
2012-05-26 22:38:58 +02:00
parent c83477d541
commit f4e2d1eba0
6 changed files with 1171 additions and 994 deletions
+26 -22
View File
@@ -13,6 +13,8 @@ import headphones
from headphones import logger, db from headphones import logger, db
from headphones.helpers import multikeysort, replace_all from headphones.helpers import multikeysort, replace_all
import lib.musicbrainzngs as musicbrainzngs
mb_lock = threading.Lock() mb_lock = threading.Lock()
@@ -46,6 +48,12 @@ def startmb(forcemb=False):
mbport = 5000 mbport = 5000
sleepytime = 0 sleepytime = 0
musicbrainzngs.set_useragent("headphones","0.0","https://github.com/doskir/headphones")
logger.info("set user agent")
musicbrainzngs.set_hostname(mbhost + ":" + str(mbport))
logger.info("set host and port")
#q = musicbrainzngs
service = ws.WebService(host=mbhost, port=mbport, username=mbuser, password=mbpass, mirror=headphones.MIRROR) service = ws.WebService(host=mbhost, port=mbport, username=mbuser, password=mbpass, mirror=headphones.MIRROR)
q = ws.Query(service) q = ws.Query(service)
@@ -56,7 +64,7 @@ def startmb(forcemb=False):
def findArtist(name, limit=1): def findArtist(name, limit=1):
with mb_lock: with mb_lock:
limit = 25
artistlist = [] artistlist = []
attempt = 0 attempt = 0
artistResults = None artistResults = None
@@ -68,11 +76,10 @@ def findArtist(name, limit=1):
q, sleepytime = startmb(forcemb=True) q, sleepytime = startmb(forcemb=True)
while attempt < 5: while attempt < 5:
try: try:
artistResults = q.getArtists(ws.ArtistFilter(query=name, limit=limit)) artistResults = musicbrainzngs.search_artists(query=name,limit=limit)['artist-list']
break break
except WebServiceError, e: except WebServiceError, e:#need to update the exceptions
logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e)))
attempt += 1 attempt += 1
time.sleep(10) time.sleep(10)
@@ -81,36 +88,33 @@ def findArtist(name, limit=1):
if not artistResults: if not artistResults:
return False return False
for result in artistResults: for result in artistResults:
if 'disambiguation' in result:
if result.artist.name != result.artist.getUniqueName() and limit == 1: uniquename = unicode(result['sort-name'] + " (" + result['disambiguation'] + ")")
else:
uniquename = unicode(result['sort-name'])
if result['name'] != uniquename and limit == 1:
logger.debug('Found an artist with a disambiguation: %s - doing an album based search' % name) logger.debug('Found an artist with a disambiguation: %s - doing an album based search' % name)
artistdict = findArtistbyAlbum(name) artistdict = findArtistbyAlbum(name)
if not artistdict: if not artistdict:
logger.debug('Cannot determine the best match from an artist/album search. Using top match instead') logger.debug('Cannot determine the best match from an artist/album search. Using top match instead')
artistlist.append({ artistlist.append({
'name': result.artist.name, 'name': unicode(result['sort-name']),
'uniquename': result.artist.getUniqueName(), 'uniquename': uniquename,
'id': u.extractUuid(result.artist.id), 'id': unicode(result['id']),
'url': result.artist.id, 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
'score': result.score 'score': int(result['ext:score'])
}) })
else: else:
artistlist.append(artistdict) artistlist.append(artistdict)
else: else:
artistlist.append({ artistlist.append({
'name': result.artist.name, 'name': unicode(result['sort-name']),
'uniquename': result.artist.getUniqueName(), 'uniquename': uniquename,
'id': u.extractUuid(result.artist.id), 'id': unicode(result['id']),
'url': result.artist.id, 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed
'score': result.score 'score': int(result['ext:score'])
}) })
return artistlist return artistlist
def findRelease(name, limit=1): def findRelease(name, limit=1):
+1 -1
View File
@@ -1 +1 @@
from musicbrainz import * from lib.musicbrainzngs.musicbrainz import *
+62
View File
@@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2012 Kenneth Reitz.
# Permission to use, copy, modify, and/or distribute this software for any
# purpose with or without fee is hereby granted, provided that the above
# copyright notice and this permission notice appear in all copies.
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
"""
pythoncompat
"""
import sys
# -------
# Pythons
# -------
# Syntax sugar.
_ver = sys.version_info
#: Python 2.x?
is_py2 = (_ver[0] == 2)
#: Python 3.x?
is_py3 = (_ver[0] == 3)
# ---------
# Specifics
# ---------
if is_py2:
from StringIO import StringIO
from urllib2 import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
HTTPHandler, build_opener, HTTPError, URLError,\
build_opener
from httplib import BadStatusLine, HTTPException
from urlparse import urlunparse
from urllib import urlencode
bytes = str
unicode = unicode
basestring = basestring
elif is_py3:
from io import StringIO
from urllib.request import HTTPPasswordMgr, HTTPDigestAuthHandler, Request,\
HTTPHandler, build_opener
from urllib.error import HTTPError, URLError
from http.client import HTTPException, BadStatusLine
from urllib.parse import urlunparse, urlencode
unicode = str
bytes = bytes
basestring = (str,bytes)
+11 -8
View File
@@ -4,9 +4,11 @@
# See the COPYING file for more information. # See the COPYING file for more information.
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import string
import StringIO
import logging import logging
from lib.musicbrainzngs import compat
from lib.musicbrainzngs import util
try: try:
from ET import fixtag from ET import fixtag
except: except:
@@ -16,7 +18,7 @@ except:
# tag and namespace declaration, if any # tag and namespace declaration, if any
if isinstance(tag, ET.QName): if isinstance(tag, ET.QName):
tag = tag.text tag = tag.text
namespace_uri, tag = string.split(tag[1:], "}", 1) namespace_uri, tag = tag[1:].split("}", 1)
prefix = namespaces.get(namespace_uri) prefix = namespaces.get(namespace_uri)
if prefix is None: if prefix is None:
prefix = "ns%d" % len(namespaces) prefix = "ns%d" % len(namespaces)
@@ -29,6 +31,7 @@ except:
xmlns = None xmlns = None
return "%s:%s" % (prefix, tag), xmlns return "%s:%s" % (prefix, tag), xmlns
NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2", NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2",
"http://musicbrainz.org/ns/ext#-2.0": "ext"} "http://musicbrainz.org/ns/ext#-2.0": "ext"}
_log = logging.getLogger("python-musicbrainz-ngs") _log = logging.getLogger("python-musicbrainz-ngs")
@@ -113,9 +116,7 @@ def parse_inner(inner_els, element):
return result return result
def parse_message(message): def parse_message(message):
s = message.read() tree = util.bytes_to_elementtree(message)
f = StringIO.StringIO(s)
tree = ET.ElementTree(file=f)
root = tree.getroot() root = tree.getroot()
result = {} result = {}
valid_elements = {"artist": parse_artist, valid_elements = {"artist": parse_artist,
@@ -176,7 +177,8 @@ def parse_artist_list(al):
def parse_artist(artist): def parse_artist(artist):
result = {} result = {}
attribs = ["id", "type", "ext:score"] attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "user-rating", "disambiguation"] elements = ["name", "sort-name", "country", "user-rating",
"disambiguation", "gender", "ipi"]
inner_els = {"life-span": parse_artist_lifespan, inner_els = {"life-span": parse_artist_lifespan,
"recording-list": parse_recording_list, "recording-list": parse_recording_list,
"release-list": parse_release_list, "release-list": parse_release_list,
@@ -199,7 +201,8 @@ def parse_label_list(ll):
def parse_label(label): def parse_label(label):
result = {} result = {}
attribs = ["id", "type", "ext:score"] attribs = ["id", "type", "ext:score"]
elements = ["name", "sort-name", "country", "label-code", "user-rating"] elements = ["name", "sort-name", "country", "label-code", "user-rating",
"ipi", "disambiguation"]
inner_els = {"life-span": parse_artist_lifespan, inner_els = {"life-span": parse_artist_lifespan,
"release-list": parse_release_list, "release-list": parse_release_list,
"tag-list": parse_tag_list, "tag-list": parse_tag_list,
+150 -79
View File
@@ -3,19 +3,18 @@
# This file is distributed under a BSD-2-Clause type license. # This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information. # See the COPYING file for more information.
import urlparse
import urllib2
import urllib
import mbxml
import re import re
import threading import threading
import time import time
import logging import logging
import httplib
import socket import socket
import xml.etree.ElementTree as etree import xml.etree.ElementTree as etree
from xml.parsers import expat from xml.parsers import expat
from lib.musicbrainzngs import mbxml
from lib.musicbrainzngs import util
from lib.musicbrainzngs import compat
_version = "0.3dev" _version = "0.3dev"
_log = logging.getLogger("musicbrainzngs") _log = logging.getLogger("musicbrainzngs")
@@ -74,6 +73,7 @@ VALID_INCLUDES = {
'puid': ["artists", "releases", "puids", "echoprints", "isrcs"], 'puid': ["artists", "releases", "puids", "echoprints", "isrcs"],
'isrc': ["artists", "releases", "puids", "echoprints", "isrcs"], 'isrc': ["artists", "releases", "puids", "echoprints", "isrcs"],
'iswc': ["artists"], 'iswc': ["artists"],
'collection': ['releases'],
} }
VALID_RELEASE_TYPES = [ VALID_RELEASE_TYPES = [
"nat", "album", "single", "ep", "compilation", "soundtrack", "spokenword", "nat", "album", "single", "ep", "compilation", "soundtrack", "spokenword",
@@ -83,29 +83,33 @@ VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"]
VALID_SEARCH_FIELDS = { VALID_SEARCH_FIELDS = {
'artist': [ 'artist': [
'arid', 'artist', 'sortname', 'type', 'begin', 'end', 'comment', 'arid', 'artist', 'sortname', 'type', 'begin', 'end', 'comment',
'alias', 'country', 'gender', 'tag' 'alias', 'country', 'gender', 'tag', 'ipi', 'artistaccent'
], ],
'release-group': [ 'release-group': [
'rgid', 'releasegroup', 'reid', 'release', 'arid', 'artist', 'rgid', 'releasegroup', 'reid', 'release', 'arid', 'artist',
'artistname', 'creditname', 'type', 'tag' 'artistname', 'creditname', 'type', 'tag', 'releasegroupaccent',
'releases', 'comment'
], ],
'release': [ 'release': [
'reid', 'release', 'arid', 'artist', 'artistname', 'creditname', 'reid', 'release', 'arid', 'artist', 'artistname', 'creditname',
'type', 'status', 'tracks', 'tracksmedium', 'discids', 'type', 'status', 'tracks', 'tracksmedium', 'discids',
'discidsmedium', 'mediums', 'date', 'asin', 'lang', 'script', 'discidsmedium', 'mediums', 'date', 'asin', 'lang', 'script',
'country', 'date', 'label', 'catno', 'barcode', 'puid' 'country', 'date', 'label', 'catno', 'barcode', 'puid', 'comment',
'format', 'releaseaccent', 'rgid'
], ],
'recording': [ 'recording': [
'rid', 'recording', 'isrc', 'arid', 'artist', 'artistname', 'rid', 'recording', 'isrc', 'arid', 'artist', 'artistname',
'creditname', 'reid', 'release', 'type', 'status', 'tracks', 'creditname', 'reid', 'release', 'type', 'status', 'tracks',
'tracksrelease', 'dur', 'qdur', 'tnum', 'position', 'tag' 'tracksrelease', 'dur', 'qdur', 'tnum', 'position', 'tag', 'comment',
'country', 'date' 'format', 'recordingaccent'
], ],
'label': [ 'label': [
'laid', 'label', 'sortname', 'type', 'code', 'country', 'begin', 'laid', 'label', 'sortname', 'type', 'code', 'country', 'begin',
'end', 'comment', 'alias', 'tag' 'end', 'comment', 'alias', 'tag', 'ipi', 'labelaccent'
], ],
'work': [ 'work': [
'wid', 'work', 'iswc', 'type', 'arid', 'artist', 'alias', 'tag' 'wid', 'work', 'iswc', 'type', 'arid', 'artist', 'alias', 'tag',
'comment', 'workaccent'
], ],
} }
@@ -186,9 +190,9 @@ def _check_filter_and_make_params(entity, includes, release_status=[], release_t
the filters can be used with the given includes. Return a params the filters can be used with the given includes. Return a params
dict that can be passed to _do_mb_query. dict that can be passed to _do_mb_query.
""" """
if isinstance(release_status, basestring): if isinstance(release_status, compat.basestring):
release_status = [release_status] release_status = [release_status]
if isinstance(release_type, basestring): if isinstance(release_type, compat.basestring):
release_type = [release_type] release_type = [release_type]
_check_filter(release_status, VALID_RELEASE_STATUSES) _check_filter(release_status, VALID_RELEASE_STATUSES)
_check_filter(release_type, VALID_RELEASE_TYPES) _check_filter(release_type, VALID_RELEASE_TYPES)
@@ -227,7 +231,7 @@ def auth(u, p):
def set_useragent(app, version, contact=None): def set_useragent(app, version, contact=None):
"""Set the User-Agent to be used for requests to the MusicBrainz webservice. """Set the User-Agent to be used for requests to the MusicBrainz webservice.
This should be set before requests are made.""" This must be set before requests are made."""
global _useragent, _client global _useragent, _client
if contact is not None: if contact is not None:
_useragent = "%s/%s python-musicbrainz-ngs/%s ( %s )" % (app, version, _version, contact) _useragent = "%s/%s python-musicbrainz-ngs/%s ( %s )" % (app, version, _version, contact)
@@ -237,6 +241,8 @@ def set_useragent(app, version, contact=None):
_log.debug("set user-agent to %s" % _useragent) _log.debug("set user-agent to %s" % _useragent)
def set_hostname(new_hostname): def set_hostname(new_hostname):
"""Set the base hostname for MusicBrainz webservice requests.
Defaults to 'musicbrainz.org'."""
global hostname global hostname
hostname = new_hostname hostname = new_hostname
@@ -244,17 +250,26 @@ def set_hostname(new_hostname):
limit_interval = 1.0 limit_interval = 1.0
limit_requests = 1 limit_requests = 1
do_rate_limit = True
def set_rate_limit(new_interval=1.0, new_requests=1): def set_rate_limit(rate_limit=True, new_interval=1.0, new_requests=1):
"""Sets the rate limiting behavior of the module. Must be invoked """Sets the rate limiting behavior of the module. Must be invoked
before the first Web service call. Specify the number of requests before the first Web service call.
(`new_requests`) that may be made per given interval If the `rate_limit` parameter is set to True, then only a set number
(`new_interval`). of requests (`new_requests`) will be made per given interval
(`new_interval`). If `rate_limit` is False, then no rate limiting
will occur.
""" """
global limit_interval global limit_interval
global limit_requests global limit_requests
global do_rate_limit
if new_interval <= 0.0:
raise ValueError("new_interval can't be less than 0")
if new_requests <= 0:
raise ValueError("new_requests can't be less than 0")
limit_interval = new_interval limit_interval = new_interval
limit_requests = new_requests limit_requests = new_requests
do_rate_limit = rate_limit
class _rate_limit(object): class _rate_limit(object):
"""A decorator that limits the rate at which the function may be """A decorator that limits the rate at which the function may be
@@ -290,6 +305,7 @@ class _rate_limit(object):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
with self.lock: with self.lock:
if do_rate_limit:
self._update_remaining() self._update_remaining()
# Delay if necessary. # Delay if necessary.
@@ -302,11 +318,8 @@ class _rate_limit(object):
self.remaining_requests -= 1.0 self.remaining_requests -= 1.0
return self.fun(*args, **kwargs) return self.fun(*args, **kwargs)
# Generic support for making HTTP requests.
# From pymb2 # From pymb2
class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr): class _RedirectPasswordMgr(compat.HTTPPasswordMgr):
def __init__(self): def __init__(self):
self._realms = { } self._realms = { }
@@ -321,18 +334,18 @@ class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr):
# ignoring the uri parameter intentionally # ignoring the uri parameter intentionally
self._realms[realm] = (username, password) self._realms[realm] = (username, password)
class _DigestAuthHandler(urllib2.HTTPDigestAuthHandler): class _DigestAuthHandler(compat.HTTPDigestAuthHandler):
def get_authorization (self, req, chal): def get_authorization (self, req, chal):
qop = chal.get ('qop', None) qop = chal.get ('qop', None)
if qop and ',' in qop and 'auth' in qop.split (','): if qop and ',' in qop and 'auth' in qop.split (','):
chal['qop'] = 'auth' chal['qop'] = 'auth'
return urllib2.HTTPDigestAuthHandler.get_authorization (self, req, chal) return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal)
class _MusicbrainzHttpRequest(urllib2.Request): class _MusicbrainzHttpRequest(compat.Request):
""" A custom request handler that allows DELETE and PUT""" """ A custom request handler that allows DELETE and PUT"""
def __init__(self, method, url, data=None): def __init__(self, method, url, data=None):
urllib2.Request.__init__(self, url, data) compat.Request.__init__(self, url, data)
allowed_m = ["GET", "POST", "DELETE", "PUT"] allowed_m = ["GET", "POST", "DELETE", "PUT"]
if method not in allowed_m: if method not in allowed_m:
raise ValueError("invalid method: %s" % method) raise ValueError("invalid method: %s" % method)
@@ -362,8 +375,8 @@ def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
else: else:
f = opener.open(req) f = opener.open(req)
except urllib2.HTTPError, exc: except compat.HTTPError as exc:
if exc.code in (400, 404): if exc.code in (400, 404, 411):
# Bad request, not found, etc. # Bad request, not found, etc.
raise ResponseError(cause=exc) raise ResponseError(cause=exc)
elif exc.code in (503, 502, 500): elif exc.code in (503, 502, 500):
@@ -374,19 +387,23 @@ def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0):
# retrying for now. # retrying for now.
_log.debug("unknown HTTP error %i" % exc.code) _log.debug("unknown HTTP error %i" % exc.code)
last_exc = exc last_exc = exc
except httplib.BadStatusLine, exc: except compat.BadStatusLine as exc:
_log.debug("bad status line") _log.debug("bad status line")
last_exc = exc last_exc = exc
except httplib.HTTPException, exc: except compat.HTTPException as exc:
_log.debug("miscellaneous HTTP exception: %s" % str(exc)) _log.debug("miscellaneous HTTP exception: %s" % str(exc))
last_exc = exc last_exc = exc
except urllib2.URLError, exc: except compat.URLError as exc:
if isinstance(exc.reason, socket.error): if isinstance(exc.reason, socket.error):
code = exc.reason.errno code = exc.reason.errno
if code == 104: # "Connection reset by peer." if code == 104: # "Connection reset by peer."
continue continue
raise NetworkError(cause=exc) raise NetworkError(cause=exc)
except IOError, exc: except socket.error as exc:
if exc.errno == 104:
continue
raise NetworkError(cause=exc)
except IOError as exc:
raise NetworkError(cause=exc) raise NetworkError(cause=exc)
else: else:
# No exception! Yay! # No exception! Yay!
@@ -425,23 +442,23 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
# Encode Unicode arguments using UTF-8. # Encode Unicode arguments using UTF-8.
for key, value in args.items(): for key, value in args.items():
if isinstance(value, unicode): if isinstance(value, compat.unicode):
args[key] = value.encode('utf8') args[key] = value.encode('utf8')
# Construct the full URL for the request, including hostname and # Construct the full URL for the request, including hostname and
# query string. # query string.
url = urlparse.urlunparse(( url = compat.urlunparse((
'http', 'http',
hostname, hostname,
'/ws/2/%s' % path, '/ws/2/%s' % path,
'', '',
urllib.urlencode(args), compat.urlencode(args),
'' ''
)) ))
_log.debug("%s request for %s" % (method, url)) _log.debug("%s request for %s" % (method, url))
# Set up HTTP request handler and URL opener. # Set up HTTP request handler and URL opener.
httpHandler = urllib2.HTTPHandler(debuglevel=0) httpHandler = compat.HTTPHandler(debuglevel=0)
handlers = [httpHandler] handlers = [httpHandler]
# Add credentials if required. # Add credentials if required.
@@ -455,7 +472,7 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
authHandler.add_password("musicbrainz.org", (), user, password) authHandler.add_password("musicbrainz.org", (), user, password)
handlers.append(authHandler) handlers.append(authHandler)
opener = urllib2.build_opener(*handlers) opener = compat.build_opener(*handlers)
# Make request. # Make request.
req = _MusicbrainzHttpRequest(method, url, data) req = _MusicbrainzHttpRequest(method, url, data)
@@ -463,6 +480,10 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False,
_log.debug("requesting with UA %s" % _useragent) _log.debug("requesting with UA %s" % _useragent)
if body: if body:
req.add_header('Content-Type', 'application/xml; charset=UTF-8') req.add_header('Content-Type', 'application/xml; charset=UTF-8')
elif not data and not req.has_header('Content-Length'):
# Explicitly indicate zero content length if no request data
# will be sent (avoids HTTP 411 error).
req.add_header('Content-Length', '0')
f = _safe_open(opener, req, body) f = _safe_open(opener, req, body)
# Parse the response. # Parse the response.
@@ -496,6 +517,8 @@ def _do_mb_query(entity, id, includes=[], params={}):
response is parsed and returned. response is parsed and returned.
""" """
# Build arguments. # Build arguments.
if not isinstance(includes, list):
includes = [includes]
_check_includes(entity, includes) _check_includes(entity, includes)
auth_required = _is_auth_required(entity, includes) auth_required = _is_auth_required(entity, includes)
args = dict(params) args = dict(params)
@@ -507,15 +530,28 @@ def _do_mb_query(entity, id, includes=[], params={}):
path = '%s/%s' % (entity, id) path = '%s/%s' % (entity, id)
return _mb_request(path, 'GET', auth_required, args=args) return _mb_request(path, 'GET', auth_required, args=args)
def _do_mb_search(entity, query='', fields={}, limit=None, offset=None): def _do_mb_search(entity, query='', fields={},
limit=None, offset=None, strict=False):
"""Perform a full-text search on the MusicBrainz search server. """Perform a full-text search on the MusicBrainz search server.
`query` is a free-form query string and `fields` is a dictionary `query` is a lucene query string when no fields are set,
but is escaped when any fields are given. `fields` is a dictionary
of key/value query parameters. They keys in `fields` must be valid of key/value query parameters. They keys in `fields` must be valid
for the given entity type. for the given entity type.
""" """
# Encode the query terms as a Lucene query string. # Encode the query terms as a Lucene query string.
query_parts = [query.replace('\x00', '').strip()] query_parts = []
for key, value in fields.iteritems(): if query:
clean_query = util._unicode(query)
if fields:
clean_query = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])',
r'\\\1', clean_query)
if strict:
query_parts.append('"%s"' % clean_query)
else:
query_parts.append(clean_query.lower())
else:
query_parts.append(clean_query)
for key, value in fields.items():
# Ensure this is a valid search field. # Ensure this is a valid search field.
if key not in VALID_SEARCH_FIELDS[entity]: if key not in VALID_SEARCH_FIELDS[entity]:
raise InvalidSearchFieldError( raise InvalidSearchFieldError(
@@ -523,12 +559,19 @@ def _do_mb_search(entity, query='', fields={}, limit=None, offset=None):
) )
# Escape Lucene's special characters. # Escape Lucene's special characters.
value = util._unicode(value)
value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', value) value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', value)
value = value.replace('\x00', '').strip()
value = value.lower() # Avoid binary operators like OR.
if value: if value:
query_parts.append(u'%s:(%s)' % (key, value)) if strict:
full_query = u' '.join(query_parts).strip() query_parts.append('%s:"%s"' % (key, value))
else:
value = value.lower() # avoid AND / OR
query_parts.append('%s:(%s)' % (key, value))
if strict:
full_query = ' AND '.join(query_parts).strip()
else:
full_query = ' '.join(query_parts).strip()
if not full_query: if not full_query:
raise ValueError('at least one query term is required') raise ValueError('at least one query term is required')
@@ -562,23 +605,23 @@ def _do_mb_post(path, body):
# Single entity by ID # Single entity by ID
def get_artist_by_id(id, includes=[], release_status=[], release_type=[]): def get_artist_by_id(id, includes=[], release_status=[], release_type=[]):
params = _check_filter_and_make_params(includes, release_status, release_type) params = _check_filter_and_make_params("artist", includes, release_status, release_type)
return _do_mb_query("artist", id, includes, params) return _do_mb_query("artist", id, includes, params)
def get_label_by_id(id, includes=[], release_status=[], release_type=[]): def get_label_by_id(id, includes=[], release_status=[], release_type=[]):
params = _check_filter_and_make_params(includes, release_status, release_type) params = _check_filter_and_make_params("label", includes, release_status, release_type)
return _do_mb_query("label", id, includes, params) return _do_mb_query("label", id, includes, params)
def get_recording_by_id(id, includes=[], release_status=[], release_type=[]): def get_recording_by_id(id, includes=[], release_status=[], release_type=[]):
params = _check_filter_and_make_params(includes, release_status, release_type) params = _check_filter_and_make_params("recording", includes, release_status, release_type)
return _do_mb_query("recording", id, includes, params) return _do_mb_query("recording", id, includes, params)
def get_release_by_id(id, includes=[], release_status=[], release_type=[]): def get_release_by_id(id, includes=[], release_status=[], release_type=[]):
params = _check_filter_and_make_params(includes, release_status, release_type) params = _check_filter_and_make_params("release", includes, release_status, release_type)
return _do_mb_query("release", id, includes, params) return _do_mb_query("release", id, includes, params)
def get_release_group_by_id(id, includes=[], release_status=[], release_type=[]): def get_release_group_by_id(id, includes=[], release_status=[], release_type=[]):
params = _check_filter_and_make_params(includes, release_status, release_type) params = _check_filter_and_make_params("release-group", includes, release_status, release_type)
return _do_mb_query("release-group", id, includes, params) return _do_mb_query("release-group", id, includes, params)
def get_work_by_id(id, includes=[]): def get_work_by_id(id, includes=[]):
@@ -587,54 +630,68 @@ def get_work_by_id(id, includes=[]):
# Searching # Searching
def search_artists(query='', limit=None, offset=None, **fields): def search_artists(query='', limit=None, offset=None, strict=False, **fields):
"""Search for artists by a free-form `query` string and/or any of """Search for artists by a free-form `query` string or any of
the following keyword arguments specifying field queries: the following keyword arguments specifying field queries:
arid, artist, sortname, type, begin, end, comment, alias, country, arid, artist, sortname, type, begin, end, comment, alias, country,
gender, tag gender, tag
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('artist', query, fields, limit, offset) return _do_mb_search('artist', query, fields, limit, offset, strict)
def search_labels(query='', limit=None, offset=None, **fields): def search_labels(query='', limit=None, offset=None, strict=False, **fields):
"""Search for labels by a free-form `query` string and/or any of """Search for labels by a free-form `query` string or any of
the following keyword arguments specifying field queries: the following keyword arguments specifying field queries:
laid, label, sortname, type, code, country, begin, end, comment, laid, label, sortname, type, code, country, begin, end, comment,
alias, tag alias, tag
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('label', query, fields, limit, offset) return _do_mb_search('label', query, fields, limit, offset, strict)
def search_recordings(query='', limit=None, offset=None, **fields): def search_recordings(query='', limit=None, offset=None, strict=False, **fields):
"""Search for recordings by a free-form `query` string and/or any of """Search for recordings by a free-form `query` string or any of
the following keyword arguments specifying field queries: the following keyword arguments specifying field queries:
rid, recording, isrc, arid, artist, artistname, creditname, reid, rid, recording, isrc, arid, artist, artistname, creditname, reid,
release, type, status, tracks, tracksrelease, dur, qdur, tnum, release, type, status, tracks, tracksrelease, dur, qdur, tnum,
position, tag position, tag
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('recording', query, fields, limit, offset) return _do_mb_search('recording', query, fields, limit, offset, strict)
def search_releases(query='', limit=None, offset=None, **fields): def search_releases(query='', limit=None, offset=None, strict=False, **fields):
"""Search for releases by a free-form `query` string and/or any of """Search for releases by a free-form `query` string or any of
the following keyword arguments specifying field queries: the following keyword arguments specifying field queries:
reid, release, arid, artist, artistname, creditname, type, status, reid, release, arid, artist, artistname, creditname, type, status,
tracks, tracksmedium, discids, discidsmedium, mediums, date, asin, tracks, tracksmedium, discids, discidsmedium, mediums, date, asin,
lang, script, country, date, label, catno, barcode, puid lang, script, country, date, label, catno, barcode, puid
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('release', query, fields, limit, offset) return _do_mb_search('release', query, fields, limit, offset, strict)
def search_release_groups(query='', limit=None, offset=None, **fields): def search_release_groups(query='', limit=None, offset=None,
"""Search for release groups by a free-form `query` string and/or strict=False, **fields):
"""Search for release groups by a free-form `query` string or
any of the following keyword arguments specifying field queries: any of the following keyword arguments specifying field queries:
rgid, releasegroup, reid, release, arid, artist, artistname, rgid, releasegroup, reid, release, arid, artist, artistname,
creditname, type, tag creditname, type, tag
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('release-group', query, fields, limit, offset) return _do_mb_search('release-group', query, fields,
limit, offset, strict)
def search_works(query='', limit=None, offset=None, **fields): def search_works(query='', limit=None, offset=None, strict=False, **fields):
"""Search for works by a free-form `query` string and/or any of """Search for works by a free-form `query` string or any of
the following keyword arguments specifying field queries: the following keyword arguments specifying field queries:
wid, work, iswc, type, arid, artist, alias, tag wid, work, iswc, type, arid, artist, alias, tag
When `fields` are set, special lucene characters are escaped
in the `query`.
""" """
return _do_mb_search('work', query, fields, limit, offset) return _do_mb_search('work', query, fields, limit, offset, strict)
# Lists of entities # Lists of entities
@@ -721,27 +778,31 @@ def get_releases_in_collection(collection):
# Submission methods # Submission methods
def submit_barcodes(barcodes): def submit_barcodes(barcodes):
""" """Submits a set of {release1: barcode1, release2:barcode2}
Submits a set of {release1: barcode1, release2:barcode2}
Must call auth(user, pass) first Must call auth(user, pass) first"""
"""
query = mbxml.make_barcode_request(barcodes) query = mbxml.make_barcode_request(barcodes)
return _do_mb_post("release", query) return _do_mb_post("release", query)
def submit_puids(puids): def submit_puids(puids):
"""Submit PUIDs.
Must call auth(user, pass) first"""
query = mbxml.make_puid_request(puids) query = mbxml.make_puid_request(puids)
return _do_mb_post("recording", query) return _do_mb_post("recording", query)
def submit_echoprints(echoprints): def submit_echoprints(echoprints):
"""Submit echoprints.
Must call auth(user, pass) first"""
query = mbxml.make_echoprint_request(echoprints) query = mbxml.make_echoprint_request(echoprints)
return _do_mb_post("recording", query) return _do_mb_post("recording", query)
def submit_isrcs(recordings_isrcs): def submit_isrcs(recordings_isrcs):
""" """Submit ISRCs.
Submit ISRCs. Submits a set of {recording-id: [isrc1, isrc1, ...]}
Submits a set of {recording-id: [isrc1, irc1]}
Must call auth(user, pass) first Must call auth(user, pass) first"""
"""
query = mbxml.make_isrc_request(recordings_isrcs=recordings_isrcs) query = mbxml.make_isrc_request(recordings_isrcs=recordings_isrcs)
return _do_mb_post("recording", query) return _do_mb_post("recording", query)
@@ -749,7 +810,8 @@ def submit_tags(artist_tags={}, recording_tags={}):
"""Submit user tags. """Submit user tags.
Artist or recording parameters are of the form: Artist or recording parameters are of the form:
{'entityid': [taglist]} {'entityid': [taglist]}
"""
Must call auth(user, pass) first"""
query = mbxml.make_tag_request(artist_tags, recording_tags) query = mbxml.make_tag_request(artist_tags, recording_tags)
return _do_mb_post("tag", query) return _do_mb_post("tag", query)
@@ -757,15 +819,24 @@ def submit_ratings(artist_ratings={}, recording_ratings={}):
""" Submit user ratings. """ Submit user ratings.
Artist or recording parameters are of the form: Artist or recording parameters are of the form:
{'entityid': rating} {'entityid': rating}
"""
Must call auth(user, pass) first"""
query = mbxml.make_rating_request(artist_ratings, recording_ratings) query = mbxml.make_rating_request(artist_ratings, recording_ratings)
return _do_mb_post("rating", query) return _do_mb_post("rating", query)
def add_releases_to_collection(collection, releases=[]): def add_releases_to_collection(collection, releases=[]):
"""Add releases to a collection.
Collection and releases should be identified by their MBIDs
Must call auth(user, pass) first"""
# XXX: Maximum URI length of 16kb means we should only allow ~400 releases # XXX: Maximum URI length of 16kb means we should only allow ~400 releases
releaselist = ";".join(releases) releaselist = ";".join(releases)
_do_mb_put("collection/%s/releases/%s" % (collection, releaselist)) _do_mb_put("collection/%s/releases/%s" % (collection, releaselist))
def remove_releases_from_collection(collection, releases=[]): def remove_releases_from_collection(collection, releases=[]):
"""Remove releases from a collection.
Collection and releases should be identified by their MBIDs
Must call auth(user, pass) first"""
releaselist = ";".join(releases) releaselist = ";".join(releases)
_do_mb_delete("collection/%s/releases/%s" % (collection, releaselist)) _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist))
+37
View File
@@ -0,0 +1,37 @@
# This file is part of the musicbrainzngs library
# Copyright (C) Alastair Porter, Adrian Sampson, and others
# This file is distributed under a BSD-2-Clause type license.
# See the COPYING file for more information.
import sys
import locale
import xml.etree.ElementTree as ET
from . import compat
def _unicode(string, encoding=None):
"""Try to decode byte strings to unicode.
This can only be a guess, but this might be better than failing.
It is safe to use this on numbers or strings that are already unicode.
"""
if isinstance(string, compat.unicode):
unicode_string = string
elif isinstance(string, compat.bytes):
# use given encoding, stdin, preferred until something != None is found
if encoding is None:
encoding = sys.stdin.encoding
if encoding is None:
encoding = locale.getpreferredencoding()
unicode_string = string.decode(encoding, "ignore")
else:
unicode_string = compat.unicode(string)
return unicode_string.replace('\x00', '').strip()
def bytes_to_elementtree(_bytes):
if compat.is_py3:
s = _unicode(_bytes.read(), "utf-8")
else:
s = _bytes.read()
f = compat.StringIO(s)
tree = ET.ElementTree(file=f)
return tree