From f4e2d1eba03fcd440f414f4898c2ed1ebfdcd3cc Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sat, 26 May 2012 22:38:58 +0200 Subject: [PATCH] Changed findArtist to use the musicbrainzngs library, the output is identical. (old musicbrainz 2 library is deprecated and broken) --- headphones/mb.py | 956 ++++++++++++------------- lib/musicbrainzngs/__init__.py | 2 +- lib/musicbrainzngs/compat.py | 62 ++ lib/musicbrainzngs/mbxml.py | 19 +- lib/musicbrainzngs/musicbrainz.py | 1089 +++++++++++++++-------------- lib/musicbrainzngs/util.py | 37 + 6 files changed, 1171 insertions(+), 994 deletions(-) create mode 100644 lib/musicbrainzngs/compat.py create mode 100644 lib/musicbrainzngs/util.py diff --git a/headphones/mb.py b/headphones/mb.py index 9aa16d7d..52c48b24 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -13,6 +13,8 @@ import headphones from headphones import logger, db from headphones.helpers import multikeysort, replace_all +import lib.musicbrainzngs as musicbrainzngs + mb_lock = threading.Lock() @@ -20,500 +22,502 @@ mb_lock = threading.Lock() # being used, so we can send those values to the log def startmb(forcemb=False): - mbuser = None - mbpass = None - - # Can use headphones mirror for queries - if headphones.MIRROR == "headphones" or "custom": - forcemb=False - - if forcemb or headphones.MIRROR == "musicbrainz.org": - mbhost = "musicbrainz.org" - mbport = 80 - sleepytime = 1 - elif headphones.MIRROR == "custom": - mbhost = headphones.CUSTOMHOST - mbport = int(headphones.CUSTOMPORT) - sleepytime = int(headphones.CUSTOMSLEEP) - elif headphones.MIRROR == "headphones": - mbhost = "178.63.142.150" - mbport = 8181 - mbuser = headphones.HPUSER - mbpass = headphones.HPPASS - sleepytime = 0 - else: - mbhost = "tbueter.com" - mbport = 5000 - sleepytime = 0 - - service = ws.WebService(host=mbhost, port=mbport, username=mbuser, password=mbpass, mirror=headphones.MIRROR) - q = ws.Query(service) - - logger.debug('Using the following server values:\nMBHost: %s ; MBPort: %i ; Sleep Interval: %i ' % (mbhost, mbport, sleepytime)) - - return (q, sleepytime) + mbuser = None + mbpass = None + + # Can use headphones mirror for queries + if headphones.MIRROR == "headphones" or "custom": + forcemb=False + + if forcemb or headphones.MIRROR == "musicbrainz.org": + mbhost = "musicbrainz.org" + mbport = 80 + sleepytime = 1 + elif headphones.MIRROR == "custom": + mbhost = headphones.CUSTOMHOST + mbport = int(headphones.CUSTOMPORT) + sleepytime = int(headphones.CUSTOMSLEEP) + elif headphones.MIRROR == "headphones": + mbhost = "178.63.142.150" + mbport = 8181 + mbuser = headphones.HPUSER + mbpass = headphones.HPPASS + sleepytime = 0 + else: + mbhost = "tbueter.com" + mbport = 5000 + 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) + q = ws.Query(service) + + logger.debug('Using the following server values:\nMBHost: %s ; MBPort: %i ; Sleep Interval: %i ' % (mbhost, mbport, sleepytime)) + + return (q, sleepytime) def findArtist(name, limit=1): - with mb_lock: - - artistlist = [] - attempt = 0 - artistResults = None - - chars = set('!?*') - if any((c in chars) for c in name): - name = '"'+name+'"' - - q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - - try: - artistResults = q.getArtists(ws.ArtistFilter(query=name, limit=limit)) - break - except WebServiceError, e: - logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) - attempt += 1 - time.sleep(10) - - time.sleep(sleepytime) - - if not artistResults: - return False - - for result in artistResults: - - if result.artist.name != result.artist.getUniqueName() and limit == 1: - - logger.debug('Found an artist with a disambiguation: %s - doing an album based search' % name) - artistdict = findArtistbyAlbum(name) - - if not artistdict: - logger.debug('Cannot determine the best match from an artist/album search. Using top match instead') - artistlist.append({ - 'name': result.artist.name, - 'uniquename': result.artist.getUniqueName(), - 'id': u.extractUuid(result.artist.id), - 'url': result.artist.id, - 'score': result.score - }) - - else: - artistlist.append(artistdict) - - else: - artistlist.append({ - 'name': result.artist.name, - 'uniquename': result.artist.getUniqueName(), - 'id': u.extractUuid(result.artist.id), - 'url': result.artist.id, - 'score': result.score - }) - - return artistlist - + with mb_lock: + limit = 25 + artistlist = [] + attempt = 0 + artistResults = None + + chars = set('!?*') + if any((c in chars) for c in name): + name = '"'+name+'"' + + q, sleepytime = startmb(forcemb=True) + + while attempt < 5: + try: + artistResults = musicbrainzngs.search_artists(query=name,limit=limit)['artist-list'] + break + except WebServiceError, e:#need to update the exceptions + logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) + attempt += 1 + time.sleep(10) + + time.sleep(sleepytime) + + if not artistResults: + return False + for result in artistResults: + if 'disambiguation' in result: + 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) + artistdict = findArtistbyAlbum(name) + if not artistdict: + logger.debug('Cannot determine the best match from an artist/album search. Using top match instead') + artistlist.append({ + 'name': unicode(result['sort-name']), + 'uniquename': uniquename, + 'id': unicode(result['id']), + 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed + 'score': int(result['ext:score']) + }) + else: + artistlist.append(artistdict) + else: + artistlist.append({ + 'name': unicode(result['sort-name']), + 'uniquename': uniquename, + 'id': unicode(result['id']), + 'url': unicode("http://musicbrainz.org/artist/" + result['id']),#probably needs to be changed + 'score': int(result['ext:score']) + }) + return artistlist + def findRelease(name, limit=1): - with mb_lock: - - releaselist = [] - attempt = 0 - releaseResults = None - - chars = set('!?') - if any((c in chars) for c in name): - name = '"'+name+'"' - - q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - - try: - releaseResults = q.getReleases(ws.ReleaseFilter(query=name, limit=limit)) - break - except WebServiceError, e: - logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) - attempt += 1 - time.sleep(10) - - time.sleep(sleepytime) - - if not releaseResults: - return False - - for result in releaseResults: - - releaselist.append({ - 'uniquename': result.release.artist.name, - 'title': result.release.title, - 'id': u.extractUuid(result.release.artist.id), - 'albumid': u.extractUuid(result.release.id), - 'url': result.release.artist.id, - 'albumurl': result.release.id, - 'score': result.score - }) - - return releaselist + with mb_lock: + + releaselist = [] + attempt = 0 + releaseResults = None + + chars = set('!?') + if any((c in chars) for c in name): + name = '"'+name+'"' + + q, sleepytime = startmb(forcemb=True) + + while attempt < 5: + + try: + releaseResults = q.getReleases(ws.ReleaseFilter(query=name, limit=limit)) + break + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) + attempt += 1 + time.sleep(10) + + time.sleep(sleepytime) + + if not releaseResults: + return False + + for result in releaseResults: + + releaselist.append({ + 'uniquename': result.release.artist.name, + 'title': result.release.title, + 'id': u.extractUuid(result.release.artist.id), + 'albumid': u.extractUuid(result.release.id), + 'url': result.release.artist.id, + 'albumurl': result.release.id, + 'score': result.score + }) + + return releaselist def getArtist(artistid, extrasonly=False): - with mb_lock: - - artist_dict = {} - - #Get all official release groups - inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True) - artist = None - attempt = 0 - - q, sleepytime = startmb() - - while attempt < 5: - - try: - artist = q.getArtistById(artistid, inc) - break - except WebServiceError, e: - logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) - attempt += 1 - time.sleep(5) - - if not artist: - return False - - time.sleep(sleepytime) - - artist_dict['artist_name'] = artist.name - artist_dict['artist_sortname'] = artist.sortName - artist_dict['artist_uniquename'] = artist.getUniqueName() - artist_dict['artist_type'] = u.extractFragment(artist.type) - artist_dict['artist_begindate'] = artist.beginDate - artist_dict['artist_enddate'] = artist.endDate - - releasegroups = [] - - if not extrasonly: - - for rg in artist.getReleaseGroups(): - - releasegroups.append({ - 'title': rg.title, - 'id': u.extractUuid(rg.id), - 'url': rg.id, - 'type': u.getReleaseTypeName(rg.type) - }) - - # See if we need to grab extras - myDB = db.DBConnection() + with mb_lock: + + artist_dict = {} + + #Get all official release groups + inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), releaseGroups=True) + artist = None + attempt = 0 + + q, sleepytime = startmb() + + while attempt < 5: + + try: + artist = q.getArtistById(artistid, inc) + break + except WebServiceError, e: + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) + attempt += 1 + time.sleep(5) + + if not artist: + return False + + time.sleep(sleepytime) + + artist_dict['artist_name'] = artist.name + artist_dict['artist_sortname'] = artist.sortName + artist_dict['artist_uniquename'] = artist.getUniqueName() + artist_dict['artist_type'] = u.extractFragment(artist.type) + artist_dict['artist_begindate'] = artist.beginDate + artist_dict['artist_enddate'] = artist.endDate + + releasegroups = [] + + if not extrasonly: + + for rg in artist.getReleaseGroups(): + + releasegroups.append({ + 'title': rg.title, + 'id': u.extractUuid(rg.id), + 'url': rg.id, + 'type': u.getReleaseTypeName(rg.type) + }) + + # See if we need to grab extras + myDB = db.DBConnection() - try: - includeExtras = myDB.select('SELECT IncludeExtras from artists WHERE ArtistID=?', [artistid])[0][0] - except IndexError: - includeExtras = False - - if includeExtras or headphones.INCLUDE_EXTRAS: - includes = [m.Release.TYPE_COMPILATION, m.Release.TYPE_REMIX, m.Release.TYPE_SINGLE, m.Release.TYPE_LIVE, m.Release.TYPE_EP, m.Release.TYPE_SOUNDTRACK] - for include in includes: - inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, include), releaseGroups=True) - - artist = None - attempt = 0 - - while attempt < 5: - - try: - artist = q.getArtistById(artistid, inc) - break - except WebServiceError, e: - logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) - attempt += 1 - time.sleep(5) - - if not artist: - continue - - for rg in artist.getReleaseGroups(): - - releasegroups.append({ - 'title': rg.title, - 'id': u.extractUuid(rg.id), - 'url': rg.id, - 'type': u.getReleaseTypeName(rg.type) - }) - - artist_dict['releasegroups'] = releasegroups - - return artist_dict - + try: + includeExtras = myDB.select('SELECT IncludeExtras from artists WHERE ArtistID=?', [artistid])[0][0] + except IndexError: + includeExtras = False + + if includeExtras or headphones.INCLUDE_EXTRAS: + includes = [m.Release.TYPE_COMPILATION, m.Release.TYPE_REMIX, m.Release.TYPE_SINGLE, m.Release.TYPE_LIVE, m.Release.TYPE_EP, m.Release.TYPE_SOUNDTRACK] + for include in includes: + inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, include), releaseGroups=True) + + artist = None + attempt = 0 + + while attempt < 5: + + try: + artist = q.getArtistById(artistid, inc) + break + except WebServiceError, e: + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) + attempt += 1 + time.sleep(5) + + if not artist: + continue + + for rg in artist.getReleaseGroups(): + + releasegroups.append({ + 'title': rg.title, + 'id': u.extractUuid(rg.id), + 'url': rg.id, + 'type': u.getReleaseTypeName(rg.type) + }) + + artist_dict['releasegroups'] = releasegroups + + return artist_dict + def getReleaseGroup(rgid): - """ - Returns a dictionary of the best stuff from a release group - """ - with mb_lock: - - releaselist = [] - - inc = ws.ReleaseGroupIncludes(releases=True, artist=True) - releaseGroup = None - attempt = 0 - - q, sleepytime = startmb() - - while attempt < 5: - - try: - releaseGroup = q.getReleaseGroupById(rgid, inc) - break - except WebServiceError, e: - logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) - attempt += 1 - time.sleep(5) - - if not releaseGroup: - return False - - time.sleep(sleepytime) - # I think for now we have to make separate queries for each release, in order - # to get more detailed release info (ASIN, track count, etc.) - for release in releaseGroup.releases: - - inc = ws.ReleaseIncludes(tracks=True, releaseEvents=True) - releaseResult = None - attempt = 0 - - while attempt < 5: - - try: - releaseResult = q.getReleaseById(release.id, inc) - break - except WebServiceError, e: - logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e))) - attempt += 1 - time.sleep(5) - - if not releaseResult: - continue - - # Release filter for non-official live albums - types = releaseResult.getTypes() - if any('Live' in type for type in types): - if not any('Official' in type for type in types): - logger.debug('%s is not an official live album. Skipping' % releaseResult.name) - continue - - time.sleep(sleepytime) - - formats = { - '2xVinyl': '2', - 'Vinyl': '2', - 'CD': '0', - 'Cassette': '3', - '2xCD': '1', - 'Digital Media': '0' - } - - country = { - 'US': '0', - 'GB': '1', - 'JP': '2', - } + """ + Returns a dictionary of the best stuff from a release group + """ + with mb_lock: + + releaselist = [] + + inc = ws.ReleaseGroupIncludes(releases=True, artist=True) + releaseGroup = None + attempt = 0 + + q, sleepytime = startmb() + + while attempt < 5: + + try: + releaseGroup = q.getReleaseGroupById(rgid, inc) + break + except WebServiceError, e: + logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) + attempt += 1 + time.sleep(5) + + if not releaseGroup: + return False + + time.sleep(sleepytime) + # I think for now we have to make separate queries for each release, in order + # to get more detailed release info (ASIN, track count, etc.) + for release in releaseGroup.releases: + + inc = ws.ReleaseIncludes(tracks=True, releaseEvents=True) + releaseResult = None + attempt = 0 + + while attempt < 5: + + try: + releaseResult = q.getReleaseById(release.id, inc) + break + except WebServiceError, e: + logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e))) + attempt += 1 + time.sleep(5) + + if not releaseResult: + continue + + # Release filter for non-official live albums + types = releaseResult.getTypes() + if any('Live' in type for type in types): + if not any('Official' in type for type in types): + logger.debug('%s is not an official live album. Skipping' % releaseResult.name) + continue + + time.sleep(sleepytime) + + formats = { + '2xVinyl': '2', + 'Vinyl': '2', + 'CD': '0', + 'Cassette': '3', + '2xCD': '1', + 'Digital Media': '0' + } + + country = { + 'US': '0', + 'GB': '1', + 'JP': '2', + } - - try: - format = int(replace_all(u.extractFragment(releaseResult.releaseEvents[0].format), formats)) - except: - format = 3 - - try: - country = int(replace_all(releaseResult.releaseEvents[0].country, country)) - except: - country = 3 - - release_dict = { - 'hasasin': bool(releaseResult.asin), - 'asin': releaseResult.asin, - 'trackscount': len(releaseResult.getTracks()), - 'releaseid': u.extractUuid(releaseResult.id), - 'releasedate': releaseResult.getEarliestReleaseDate(), - 'format': format, - 'country': country - } - - tracks = [] - - i = 1 - for track in releaseResult.tracks: - - tracks.append({ - 'number': i, - 'title': track.title, - 'id': u.extractUuid(track.id), - 'url': track.id, - 'duration': track.duration - }) - i += 1 - - release_dict['tracks'] = tracks - - releaselist.append(release_dict) - - average_tracks = sum(x['trackscount'] for x in releaselist) / float(len(releaselist)) - - for item in releaselist: - item['trackscount_delta'] = abs(average_tracks - item['trackscount']) - - a = multikeysort(releaselist, ['-hasasin', 'country', 'format', 'trackscount_delta']) - - release_dict = {'releaseid' :a[0]['releaseid'], - 'releasedate' : releaselist[0]['releasedate'], - 'trackcount' : a[0]['trackscount'], - 'tracks' : a[0]['tracks'], - 'asin' : a[0]['asin'], - 'releaselist' : releaselist, - 'artist_name' : releaseGroup.artist.name, - 'artist_id' : u.extractUuid(releaseGroup.artist.id), - 'title' : releaseGroup.title, - 'type' : u.extractFragment(releaseGroup.type) - } - - return release_dict - + + try: + format = int(replace_all(u.extractFragment(releaseResult.releaseEvents[0].format), formats)) + except: + format = 3 + + try: + country = int(replace_all(releaseResult.releaseEvents[0].country, country)) + except: + country = 3 + + release_dict = { + 'hasasin': bool(releaseResult.asin), + 'asin': releaseResult.asin, + 'trackscount': len(releaseResult.getTracks()), + 'releaseid': u.extractUuid(releaseResult.id), + 'releasedate': releaseResult.getEarliestReleaseDate(), + 'format': format, + 'country': country + } + + tracks = [] + + i = 1 + for track in releaseResult.tracks: + + tracks.append({ + 'number': i, + 'title': track.title, + 'id': u.extractUuid(track.id), + 'url': track.id, + 'duration': track.duration + }) + i += 1 + + release_dict['tracks'] = tracks + + releaselist.append(release_dict) + + average_tracks = sum(x['trackscount'] for x in releaselist) / float(len(releaselist)) + + for item in releaselist: + item['trackscount_delta'] = abs(average_tracks - item['trackscount']) + + a = multikeysort(releaselist, ['-hasasin', 'country', 'format', 'trackscount_delta']) + + release_dict = {'releaseid' :a[0]['releaseid'], + 'releasedate' : releaselist[0]['releasedate'], + 'trackcount' : a[0]['trackscount'], + 'tracks' : a[0]['tracks'], + 'asin' : a[0]['asin'], + 'releaselist' : releaselist, + 'artist_name' : releaseGroup.artist.name, + 'artist_id' : u.extractUuid(releaseGroup.artist.id), + 'title' : releaseGroup.title, + 'type' : u.extractFragment(releaseGroup.type) + } + + return release_dict + def getRelease(releaseid): - """ - Deep release search to get track info - """ - with mb_lock: - - release = {} - - inc = ws.ReleaseIncludes(tracks=True, releaseEvents=True, releaseGroup=True, artist=True) - results = None - attempt = 0 - - q, sleepytime = startmb() - - while attempt < 5: - - try: - results = q.getReleaseById(releaseid, inc) - break - except WebServiceError, e: - logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) - attempt += 1 - time.sleep(5) - - if not results: - return False - - time.sleep(sleepytime) - - release['title'] = results.title - release['id'] = u.extractUuid(results.id) - release['asin'] = results.asin - release['date'] = results.getEarliestReleaseDate() + """ + Deep release search to get track info + """ + with mb_lock: + + release = {} + + inc = ws.ReleaseIncludes(tracks=True, releaseEvents=True, releaseGroup=True, artist=True) + results = None + attempt = 0 + + q, sleepytime = startmb() + + while attempt < 5: + + try: + results = q.getReleaseById(releaseid, inc) + break + except WebServiceError, e: + logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) + attempt += 1 + time.sleep(5) + + if not results: + return False + + time.sleep(sleepytime) + + release['title'] = results.title + release['id'] = u.extractUuid(results.id) + release['asin'] = results.asin + release['date'] = results.getEarliestReleaseDate() - rg = results.getReleaseGroup() - if rg: - release['rgid'] = u.extractUuid(rg.id) - release['rg_title'] = rg.title - release['rg_type'] = u.extractFragment(rg.type) - else: - logger.warn("Release " + releaseid + "had no ReleaseGroup associated") - #so we can start with a releaseID from anywhere and get the artist info - #it looks like MB api v1 only returns 1 artist object - 2.0 returns more... - release['artist_name'] = results.artist.name - release['artist_id'] = u.extractUuid(results.artist.id) - - tracks = [] - - i = 1 - for track in results.tracks: - tracks.append({ - 'number': i, - 'title': track.title, - 'id': u.extractUuid(track.id), - 'url': track.id, - 'duration': track.duration - }) - i += 1 - - release['tracks'] = tracks - - return release + rg = results.getReleaseGroup() + if rg: + release['rgid'] = u.extractUuid(rg.id) + release['rg_title'] = rg.title + release['rg_type'] = u.extractFragment(rg.type) + else: + logger.warn("Release " + releaseid + "had no ReleaseGroup associated") + #so we can start with a releaseID from anywhere and get the artist info + #it looks like MB api v1 only returns 1 artist object - 2.0 returns more... + release['artist_name'] = results.artist.name + release['artist_id'] = u.extractUuid(results.artist.id) + + tracks = [] + + i = 1 + for track in results.tracks: + tracks.append({ + 'number': i, + 'title': track.title, + 'id': u.extractUuid(track.id), + 'url': track.id, + 'duration': track.duration + }) + i += 1 + + release['tracks'] = tracks + + return release # Used when there is a disambiguation def findArtistbyAlbum(name): - myDB = db.DBConnection() - - artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone() - - if not artist: - return False - - # Probably not neccessary but just want to double check - if not artist['AlbumTitle']: - return False - - term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"' - - f = ws.ReleaseGroupFilter(query=term, limit=1) - results = None - attempt = 0 - - q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - - try: - results = q.getReleaseGroups(f) - break - except WebServiceError, e: - logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) - attempt += 1 - time.sleep(10) - - time.sleep(sleepytime) - - if not results: - return False + myDB = db.DBConnection() + + artist = myDB.action('SELECT AlbumTitle from have WHERE ArtistName=? AND AlbumTitle IS NOT NULL ORDER BY RANDOM()', [name]).fetchone() + + if not artist: + return False + + # Probably not neccessary but just want to double check + if not artist['AlbumTitle']: + return False + + term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"' + + f = ws.ReleaseGroupFilter(query=term, limit=1) + results = None + attempt = 0 + + q, sleepytime = startmb(forcemb=True) + + while attempt < 5: + + try: + results = q.getReleaseGroups(f) + break + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) + attempt += 1 + time.sleep(10) + + time.sleep(sleepytime) + + if not results: + return False - artist_dict = {} - - for result in results: - releaseGroup = result.releaseGroup - artist_dict['name'] = releaseGroup.artist.name - artist_dict['uniquename'] = releaseGroup.artist.getUniqueName() - artist_dict['id'] = u.extractUuid(releaseGroup.artist.id) - artist_dict['url'] = releaseGroup.artist.id - artist_dict['score'] = result.score - - return artist_dict - + artist_dict = {} + + for result in results: + releaseGroup = result.releaseGroup + artist_dict['name'] = releaseGroup.artist.name + artist_dict['uniquename'] = releaseGroup.artist.getUniqueName() + artist_dict['id'] = u.extractUuid(releaseGroup.artist.id) + artist_dict['url'] = releaseGroup.artist.id + artist_dict['score'] = result.score + + return artist_dict + def findAlbumID(artist=None, album=None): - f = ws.ReleaseGroupFilter(title=album, artistName=artist, limit=1) - results = None - attempt = 0 - - q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - - try: - results = q.getReleaseGroups(f) - break - except WebServiceError, e: - logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) - attempt += 1 - time.sleep(10) - - time.sleep(sleepytime) - - if not results: - return False - - rgid = u.extractUuid(results[0].releaseGroup.id) - return rgid + f = ws.ReleaseGroupFilter(title=album, artistName=artist, limit=1) + results = None + attempt = 0 + + q, sleepytime = startmb(forcemb=True) + + while attempt < 5: + + try: + results = q.getReleaseGroups(f) + break + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) + attempt += 1 + time.sleep(10) + + time.sleep(sleepytime) + + if not results: + return False + + rgid = u.extractUuid(results[0].releaseGroup.id) + return rgid diff --git a/lib/musicbrainzngs/__init__.py b/lib/musicbrainzngs/__init__.py index c58a0dd8..40a89036 100644 --- a/lib/musicbrainzngs/__init__.py +++ b/lib/musicbrainzngs/__init__.py @@ -1 +1 @@ -from musicbrainz import * +from lib.musicbrainzngs.musicbrainz import * diff --git a/lib/musicbrainzngs/compat.py b/lib/musicbrainzngs/compat.py new file mode 100644 index 00000000..36574b5c --- /dev/null +++ b/lib/musicbrainzngs/compat.py @@ -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) diff --git a/lib/musicbrainzngs/mbxml.py b/lib/musicbrainzngs/mbxml.py index 951eea84..c4e46e96 100644 --- a/lib/musicbrainzngs/mbxml.py +++ b/lib/musicbrainzngs/mbxml.py @@ -4,9 +4,11 @@ # See the COPYING file for more information. import xml.etree.ElementTree as ET -import string -import StringIO import logging + +from lib.musicbrainzngs import compat +from lib.musicbrainzngs import util + try: from ET import fixtag except: @@ -16,7 +18,7 @@ except: # tag and namespace declaration, if any if isinstance(tag, ET.QName): tag = tag.text - namespace_uri, tag = string.split(tag[1:], "}", 1) + namespace_uri, tag = tag[1:].split("}", 1) prefix = namespaces.get(namespace_uri) if prefix is None: prefix = "ns%d" % len(namespaces) @@ -29,6 +31,7 @@ except: xmlns = None return "%s:%s" % (prefix, tag), xmlns + NS_MAP = {"http://musicbrainz.org/ns/mmd-2.0#": "ws2", "http://musicbrainz.org/ns/ext#-2.0": "ext"} _log = logging.getLogger("python-musicbrainz-ngs") @@ -113,9 +116,7 @@ def parse_inner(inner_els, element): return result def parse_message(message): - s = message.read() - f = StringIO.StringIO(s) - tree = ET.ElementTree(file=f) + tree = util.bytes_to_elementtree(message) root = tree.getroot() result = {} valid_elements = {"artist": parse_artist, @@ -176,7 +177,8 @@ def parse_artist_list(al): def parse_artist(artist): result = {} 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, "recording-list": parse_recording_list, "release-list": parse_release_list, @@ -199,7 +201,8 @@ def parse_label_list(ll): def parse_label(label): result = {} 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, "release-list": parse_release_list, "tag-list": parse_tag_list, diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index 4f8fc9cc..88c54fa7 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -3,19 +3,18 @@ # This file is distributed under a BSD-2-Clause type license. # See the COPYING file for more information. -import urlparse -import urllib2 -import urllib -import mbxml import re import threading import time import logging -import httplib import socket import xml.etree.ElementTree as etree from xml.parsers import expat +from lib.musicbrainzngs import mbxml +from lib.musicbrainzngs import util +from lib.musicbrainzngs import compat + _version = "0.3dev" _log = logging.getLogger("musicbrainzngs") @@ -23,148 +22,153 @@ _log = logging.getLogger("musicbrainzngs") # Constants for validation. VALID_INCLUDES = { - 'artist': [ - "recordings", "releases", "release-groups", "works", # Subqueries - "various-artists", "discids", "media", - "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels" - ], - 'label': [ - "releases", # Subqueries - "discids", "media", - "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels" - ], - 'recording': [ - "artists", "releases", # Subqueries - "discids", "media", "artist-credits", - "tags", "user-tags", "ratings", "user-ratings", # misc - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels" - ], - 'release': [ - "artists", "labels", "recordings", "release-groups", "media", - "artist-credits", "discids", "puids", "echoprints", "isrcs", - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels", "recording-level-rels", - "work-level-rels" - ], - 'release-group': [ - "artists", "releases", "discids", "media", - "artist-credits", "tags", "user-tags", "ratings", "user-ratings", # misc - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels" - ], - 'work': [ - "artists", # Subqueries - "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels" - ], - 'discid': [ - "artists", "labels", "recordings", "release-groups", "media", - "artist-credits", "discids", "puids", "echoprints", "isrcs", - "artist-rels", "label-rels", "recording-rels", "release-rels", - "release-group-rels", "url-rels", "work-rels", "recording-level-rels", - "work-level-rels" - ], - 'echoprint': ["artists", "releases"], - 'puid': ["artists", "releases", "puids", "echoprints", "isrcs"], - 'isrc': ["artists", "releases", "puids", "echoprints", "isrcs"], - 'iswc': ["artists"], + 'artist': [ + "recordings", "releases", "release-groups", "works", # Subqueries + "various-artists", "discids", "media", + "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels" + ], + 'label': [ + "releases", # Subqueries + "discids", "media", + "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels" + ], + 'recording': [ + "artists", "releases", # Subqueries + "discids", "media", "artist-credits", + "tags", "user-tags", "ratings", "user-ratings", # misc + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels" + ], + 'release': [ + "artists", "labels", "recordings", "release-groups", "media", + "artist-credits", "discids", "puids", "echoprints", "isrcs", + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels", "recording-level-rels", + "work-level-rels" + ], + 'release-group': [ + "artists", "releases", "discids", "media", + "artist-credits", "tags", "user-tags", "ratings", "user-ratings", # misc + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels" + ], + 'work': [ + "artists", # Subqueries + "aliases", "tags", "user-tags", "ratings", "user-ratings", # misc + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels" + ], + 'discid': [ + "artists", "labels", "recordings", "release-groups", "media", + "artist-credits", "discids", "puids", "echoprints", "isrcs", + "artist-rels", "label-rels", "recording-rels", "release-rels", + "release-group-rels", "url-rels", "work-rels", "recording-level-rels", + "work-level-rels" + ], + 'echoprint': ["artists", "releases"], + 'puid': ["artists", "releases", "puids", "echoprints", "isrcs"], + 'isrc': ["artists", "releases", "puids", "echoprints", "isrcs"], + 'iswc': ["artists"], + 'collection': ['releases'], } VALID_RELEASE_TYPES = [ - "nat", "album", "single", "ep", "compilation", "soundtrack", "spokenword", - "interview", "audiobook", "live", "remix", "other" + "nat", "album", "single", "ep", "compilation", "soundtrack", "spokenword", + "interview", "audiobook", "live", "remix", "other" ] VALID_RELEASE_STATUSES = ["official", "promotion", "bootleg", "pseudo-release"] VALID_SEARCH_FIELDS = { - 'artist': [ - 'arid', 'artist', 'sortname', 'type', 'begin', 'end', 'comment', - 'alias', 'country', 'gender', 'tag' - ], - 'release-group': [ - 'rgid', 'releasegroup', 'reid', 'release', 'arid', 'artist', - 'artistname', 'creditname', 'type', 'tag' - ], - 'release': [ - 'reid', 'release', 'arid', 'artist', 'artistname', 'creditname', - 'type', 'status', 'tracks', 'tracksmedium', 'discids', - 'discidsmedium', 'mediums', 'date', 'asin', 'lang', 'script', - 'country', 'date', 'label', 'catno', 'barcode', 'puid' - ], - 'recording': [ - 'rid', 'recording', 'isrc', 'arid', 'artist', 'artistname', - 'creditname', 'reid', 'release', 'type', 'status', 'tracks', - 'tracksrelease', 'dur', 'qdur', 'tnum', 'position', 'tag' - ], - 'label': [ - 'laid', 'label', 'sortname', 'type', 'code', 'country', 'begin', - 'end', 'comment', 'alias', 'tag' - ], - 'work': [ - 'wid', 'work', 'iswc', 'type', 'arid', 'artist', 'alias', 'tag' - ], + 'artist': [ + 'arid', 'artist', 'sortname', 'type', 'begin', 'end', 'comment', + 'alias', 'country', 'gender', 'tag', 'ipi', 'artistaccent' + ], + 'release-group': [ + 'rgid', 'releasegroup', 'reid', 'release', 'arid', 'artist', + 'artistname', 'creditname', 'type', 'tag', 'releasegroupaccent', + 'releases', 'comment' + ], + 'release': [ + 'reid', 'release', 'arid', 'artist', 'artistname', 'creditname', + 'type', 'status', 'tracks', 'tracksmedium', 'discids', + 'discidsmedium', 'mediums', 'date', 'asin', 'lang', 'script', + 'country', 'date', 'label', 'catno', 'barcode', 'puid', 'comment', + 'format', 'releaseaccent', 'rgid' + ], + 'recording': [ + 'rid', 'recording', 'isrc', 'arid', 'artist', 'artistname', + 'creditname', 'reid', 'release', 'type', 'status', 'tracks', + 'tracksrelease', 'dur', 'qdur', 'tnum', 'position', 'tag', 'comment', + 'country', 'date' 'format', 'recordingaccent' + ], + 'label': [ + 'laid', 'label', 'sortname', 'type', 'code', 'country', 'begin', + 'end', 'comment', 'alias', 'tag', 'ipi', 'labelaccent' + ], + 'work': [ + 'wid', 'work', 'iswc', 'type', 'arid', 'artist', 'alias', 'tag', + 'comment', 'workaccent' + ], } # Exceptions. class MusicBrainzError(Exception): - """Base class for all exceptions related to MusicBrainz.""" - pass + """Base class for all exceptions related to MusicBrainz.""" + pass class UsageError(MusicBrainzError): - """Error related to misuse of the module API.""" - pass + """Error related to misuse of the module API.""" + pass class InvalidSearchFieldError(UsageError): - pass + pass class InvalidIncludeError(UsageError): - def __init__(self, msg='Invalid Includes', reason=None): - super(InvalidIncludeError, self).__init__(self) - self.msg = msg - self.reason = reason + def __init__(self, msg='Invalid Includes', reason=None): + super(InvalidIncludeError, self).__init__(self) + self.msg = msg + self.reason = reason - def __str__(self): - return self.msg + def __str__(self): + return self.msg class InvalidFilterError(UsageError): - def __init__(self, msg='Invalid Includes', reason=None): - super(InvalidFilterError, self).__init__(self) - self.msg = msg - self.reason = reason + def __init__(self, msg='Invalid Includes', reason=None): + super(InvalidFilterError, self).__init__(self) + self.msg = msg + self.reason = reason - def __str__(self): - return self.msg + def __str__(self): + return self.msg class WebServiceError(MusicBrainzError): - """Error related to MusicBrainz API requests.""" - def __init__(self, message=None, cause=None): - """Pass ``cause`` if this exception was caused by another - exception. - """ - self.message = message - self.cause = cause + """Error related to MusicBrainz API requests.""" + def __init__(self, message=None, cause=None): + """Pass ``cause`` if this exception was caused by another + exception. + """ + self.message = message + self.cause = cause - def __str__(self): - if self.message: - msg = "%s, " % self.message - else: - msg = "" - msg += "caused by: %s" % str(self.cause) - return msg + def __str__(self): + if self.message: + msg = "%s, " % self.message + else: + msg = "" + msg += "caused by: %s" % str(self.cause) + return msg class NetworkError(WebServiceError): - """Problem communicating with the MB server.""" - pass + """Problem communicating with the MB server.""" + pass class ResponseError(WebServiceError): - """Bad response sent by the MB server.""" - pass + """Bad response sent by the MB server.""" + pass # Helpers for validating and formatting allowed sets. @@ -177,37 +181,37 @@ def _check_includes(entity, inc): _check_includes_impl(inc, VALID_INCLUDES[entity]) def _check_filter(values, valid): - for v in values: - if v not in valid: - raise InvalidFilterError(v) + for v in values: + if v not in valid: + raise InvalidFilterError(v) def _check_filter_and_make_params(entity, includes, release_status=[], release_type=[]): - """Check that the status or type values are valid. Then, check that - the filters can be used with the given includes. Return a params - dict that can be passed to _do_mb_query. - """ - if isinstance(release_status, basestring): - release_status = [release_status] - if isinstance(release_type, basestring): - release_type = [release_type] - _check_filter(release_status, VALID_RELEASE_STATUSES) - _check_filter(release_type, VALID_RELEASE_TYPES) + """Check that the status or type values are valid. Then, check that + the filters can be used with the given includes. Return a params + dict that can be passed to _do_mb_query. + """ + if isinstance(release_status, compat.basestring): + release_status = [release_status] + if isinstance(release_type, compat.basestring): + release_type = [release_type] + _check_filter(release_status, VALID_RELEASE_STATUSES) + _check_filter(release_type, VALID_RELEASE_TYPES) - if release_status and "releases" not in includes: - raise InvalidFilterError("Can't have a status with no release include") - if release_type and ("release-groups" not in includes and - "releases" not in includes and - entity != "release-group"): - raise InvalidFilterError("Can't have a release type with no " - "release-group include") + if release_status and "releases" not in includes: + raise InvalidFilterError("Can't have a status with no release include") + if release_type and ("release-groups" not in includes and + "releases" not in includes and + entity != "release-group"): + raise InvalidFilterError("Can't have a release type with no " + "release-group include") - # Build parameters. - params = {} - if len(release_status): - params["status"] = "|".join(release_status) - if len(release_type): - params["type"] = "|".join(release_type) - return params + # Build parameters. + params = {} + if len(release_status): + params["status"] = "|".join(release_status) + if len(release_type): + params["type"] = "|".join(release_type) + return params # Global authentication and endpoint details. @@ -218,16 +222,16 @@ _client = "" _useragent = "" def auth(u, p): - """Set the username and password to be used in subsequent queries to - the MusicBrainz XML API that require authentication. - """ - global user, password - user = u - password = p + """Set the username and password to be used in subsequent queries to + the MusicBrainz XML API that require authentication. + """ + global user, password + user = u + password = p def set_useragent(app, version, contact=None): - """ Set the User-Agent to be used for requests to the MusicBrainz webservice. - This should be set before requests are made.""" + """Set the User-Agent to be used for requests to the MusicBrainz webservice. + This must be set before requests are made.""" global _useragent, _client if contact is not None: _useragent = "%s/%s python-musicbrainz-ngs/%s ( %s )" % (app, version, _version, contact) @@ -237,425 +241,478 @@ def set_useragent(app, version, contact=None): _log.debug("set user-agent to %s" % _useragent) def set_hostname(new_hostname): - global hostname - hostname = new_hostname + """Set the base hostname for MusicBrainz webservice requests. + Defaults to 'musicbrainz.org'.""" + global hostname + hostname = new_hostname # Rate limiting. limit_interval = 1.0 limit_requests = 1 +do_rate_limit = True -def set_rate_limit(new_interval=1.0, new_requests=1): - """Sets the rate limiting behavior of the module. Must be invoked - before the first Web service call. Specify the number of requests - (`new_requests`) that may be made per given interval - (`new_interval`). - """ - global limit_interval - global limit_requests - limit_interval = new_interval - limit_requests = new_requests +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 + before the first Web service call. + If the `rate_limit` parameter is set to True, then only a set number + 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_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_requests = new_requests + do_rate_limit = rate_limit class _rate_limit(object): - """A decorator that limits the rate at which the function may be - called. The rate is controlled by the `limit_interval` and - `limit_requests` global variables. The limiting is thread-safe; - only one thread may be in the function at a time (acts like a - monitor in this sense). The globals must be set before the first - call to the limited function. - """ - def __init__(self, fun): - self.fun = fun - self.last_call = 0.0 - self.lock = threading.Lock() - self.remaining_requests = None # Set on first invocation. + """A decorator that limits the rate at which the function may be + called. The rate is controlled by the `limit_interval` and + `limit_requests` global variables. The limiting is thread-safe; + only one thread may be in the function at a time (acts like a + monitor in this sense). The globals must be set before the first + call to the limited function. + """ + def __init__(self, fun): + self.fun = fun + self.last_call = 0.0 + self.lock = threading.Lock() + self.remaining_requests = None # Set on first invocation. - def _update_remaining(self): - """Update remaining requests based on the elapsed time since - they were last calculated. - """ - # On first invocation, we have the maximum number of requests - # available. - if self.remaining_requests is None: - self.remaining_requests = float(limit_requests) + def _update_remaining(self): + """Update remaining requests based on the elapsed time since + they were last calculated. + """ + # On first invocation, we have the maximum number of requests + # available. + if self.remaining_requests is None: + self.remaining_requests = float(limit_requests) - else: - since_last_call = time.time() - self.last_call - self.remaining_requests += since_last_call * \ - (limit_requests / limit_interval) - self.remaining_requests = min(self.remaining_requests, - float(limit_requests)) + else: + since_last_call = time.time() - self.last_call + self.remaining_requests += since_last_call * \ + (limit_requests / limit_interval) + self.remaining_requests = min(self.remaining_requests, + float(limit_requests)) - self.last_call = time.time() + self.last_call = time.time() - def __call__(self, *args, **kwargs): - with self.lock: - self._update_remaining() + def __call__(self, *args, **kwargs): + with self.lock: + if do_rate_limit: + self._update_remaining() - # Delay if necessary. - while self.remaining_requests < 0.999: - time.sleep((1.0 - self.remaining_requests) * - (limit_requests / limit_interval)) - self._update_remaining() + # Delay if necessary. + while self.remaining_requests < 0.999: + time.sleep((1.0 - self.remaining_requests) * + (limit_requests / limit_interval)) + self._update_remaining() - # Call the original function, "paying" for this call. - self.remaining_requests -= 1.0 - return self.fun(*args, **kwargs) - - -# Generic support for making HTTP requests. + # Call the original function, "paying" for this call. + self.remaining_requests -= 1.0 + return self.fun(*args, **kwargs) # From pymb2 -class _RedirectPasswordMgr(urllib2.HTTPPasswordMgr): - def __init__(self): - self._realms = { } +class _RedirectPasswordMgr(compat.HTTPPasswordMgr): + def __init__(self): + self._realms = { } - def find_user_password(self, realm, uri): - # ignoring the uri parameter intentionally - try: - return self._realms[realm] - except KeyError: - return (None, None) + def find_user_password(self, realm, uri): + # ignoring the uri parameter intentionally + try: + return self._realms[realm] + except KeyError: + return (None, None) - def add_password(self, realm, uri, username, password): - # ignoring the uri parameter intentionally - self._realms[realm] = (username, password) + def add_password(self, realm, uri, username, password): + # ignoring the uri parameter intentionally + self._realms[realm] = (username, password) -class _DigestAuthHandler(urllib2.HTTPDigestAuthHandler): - def get_authorization (self, req, chal): - qop = chal.get ('qop', None) - if qop and ',' in qop and 'auth' in qop.split (','): - chal['qop'] = 'auth' +class _DigestAuthHandler(compat.HTTPDigestAuthHandler): + def get_authorization (self, req, chal): + qop = chal.get ('qop', None) + if qop and ',' in qop and 'auth' in qop.split (','): + chal['qop'] = 'auth' - return urllib2.HTTPDigestAuthHandler.get_authorization (self, req, chal) + return compat.HTTPDigestAuthHandler.get_authorization (self, req, chal) -class _MusicbrainzHttpRequest(urllib2.Request): - """ A custom request handler that allows DELETE and PUT""" - def __init__(self, method, url, data=None): - urllib2.Request.__init__(self, url, data) - allowed_m = ["GET", "POST", "DELETE", "PUT"] - if method not in allowed_m: - raise ValueError("invalid method: %s" % method) - self.method = method +class _MusicbrainzHttpRequest(compat.Request): + """ A custom request handler that allows DELETE and PUT""" + def __init__(self, method, url, data=None): + compat.Request.__init__(self, url, data) + allowed_m = ["GET", "POST", "DELETE", "PUT"] + if method not in allowed_m: + raise ValueError("invalid method: %s" % method) + self.method = method - def get_method(self): - return self.method + def get_method(self): + return self.method # Core (internal) functions for calling the MB API. def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0): - """Open an HTTP request with a given URL opener and (optionally) a - request body. Transient errors lead to retries. Permanent errors - and repeated errors are translated into a small set of handleable - exceptions. Returns a file-like object. - """ - last_exc = None - for retry_num in range(max_retries): - if retry_num: # Not the first try: delay an increasing amount. - _log.debug("retrying after delay (#%i)" % retry_num) - time.sleep(retry_num * retry_delay_delta) + """Open an HTTP request with a given URL opener and (optionally) a + request body. Transient errors lead to retries. Permanent errors + and repeated errors are translated into a small set of handleable + exceptions. Returns a file-like object. + """ + last_exc = None + for retry_num in range(max_retries): + if retry_num: # Not the first try: delay an increasing amount. + _log.debug("retrying after delay (#%i)" % retry_num) + time.sleep(retry_num * retry_delay_delta) - try: - if body: - f = opener.open(req, body) - else: - f = opener.open(req) + try: + if body: + f = opener.open(req, body) + else: + f = opener.open(req) - except urllib2.HTTPError, exc: - if exc.code in (400, 404): - # Bad request, not found, etc. - raise ResponseError(cause=exc) - elif exc.code in (503, 502, 500): - # Rate limiting, internal overloading... - _log.debug("HTTP error %i" % exc.code) - else: - # Other, unknown error. Should handle more cases, but - # retrying for now. - _log.debug("unknown HTTP error %i" % exc.code) - last_exc = exc - except httplib.BadStatusLine, exc: - _log.debug("bad status line") - last_exc = exc - except httplib.HTTPException, exc: - _log.debug("miscellaneous HTTP exception: %s" % str(exc)) - last_exc = exc - except urllib2.URLError, exc: - if isinstance(exc.reason, socket.error): - code = exc.reason.errno - if code == 104: # "Connection reset by peer." - continue - raise NetworkError(cause=exc) - except IOError, exc: - raise NetworkError(cause=exc) - else: - # No exception! Yay! - return f + except compat.HTTPError as exc: + if exc.code in (400, 404, 411): + # Bad request, not found, etc. + raise ResponseError(cause=exc) + elif exc.code in (503, 502, 500): + # Rate limiting, internal overloading... + _log.debug("HTTP error %i" % exc.code) + else: + # Other, unknown error. Should handle more cases, but + # retrying for now. + _log.debug("unknown HTTP error %i" % exc.code) + last_exc = exc + except compat.BadStatusLine as exc: + _log.debug("bad status line") + last_exc = exc + except compat.HTTPException as exc: + _log.debug("miscellaneous HTTP exception: %s" % str(exc)) + last_exc = exc + except compat.URLError as exc: + if isinstance(exc.reason, socket.error): + code = exc.reason.errno + if code == 104: # "Connection reset by peer." + continue + raise NetworkError(cause=exc) + except socket.error as exc: + if exc.errno == 104: + continue + raise NetworkError(cause=exc) + except IOError as exc: + raise NetworkError(cause=exc) + else: + # No exception! Yay! + return f - # Out of retries! - raise NetworkError("retried %i times" % max_retries, last_exc) + # Out of retries! + raise NetworkError("retried %i times" % max_retries, last_exc) # Get the XML parsing exceptions to catch. The behavior chnaged with Python 2.7 # and ElementTree 1.3. if hasattr(etree, 'ParseError'): - ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError) + ETREE_EXCEPTIONS = (etree.ParseError, expat.ExpatError) else: - ETREE_EXCEPTIONS = (expat.ExpatError) + ETREE_EXCEPTIONS = (expat.ExpatError) @_rate_limit def _mb_request(path, method='GET', auth_required=False, 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. - """ - if args is None: - args = {} - else: - args = dict(args) or {} + 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. + """ + if args is None: + args = {} + else: + args = dict(args) or {} - if _useragent == "": - raise UsageError("set a proper user-agent with " - "set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")") + if _useragent == "": + raise UsageError("set a proper user-agent with " + "set_useragent(\"application name\", \"application version\", \"contact info (preferably URL or email for your application)\")") - if client_required: - args["client"] = _client + if client_required: + args["client"] = _client - # Encode Unicode arguments using UTF-8. - for key, value in args.items(): - if isinstance(value, unicode): - args[key] = value.encode('utf8') + # Encode Unicode arguments using UTF-8. + for key, value in args.items(): + if isinstance(value, compat.unicode): + args[key] = value.encode('utf8') - # Construct the full URL for the request, including hostname and - # query string. - url = urlparse.urlunparse(( - 'http', - hostname, - '/ws/2/%s' % path, - '', - urllib.urlencode(args), - '' - )) - _log.debug("%s request for %s" % (method, url)) + # Construct the full URL for the request, including hostname and + # query string. + url = compat.urlunparse(( + 'http', + hostname, + '/ws/2/%s' % path, + '', + compat.urlencode(args), + '' + )) + _log.debug("%s request for %s" % (method, url)) - # Set up HTTP request handler and URL opener. - httpHandler = urllib2.HTTPHandler(debuglevel=0) - handlers = [httpHandler] + # Set up HTTP request handler and URL opener. + httpHandler = compat.HTTPHandler(debuglevel=0) + handlers = [httpHandler] - # Add credentials if required. - if auth_required: - _log.debug("Auth required for %s" % url) - if not user: - raise UsageError("authorization required; " - "use auth(user, pass) first") - passwordMgr = _RedirectPasswordMgr() - authHandler = _DigestAuthHandler(passwordMgr) - authHandler.add_password("musicbrainz.org", (), user, password) - handlers.append(authHandler) + # Add credentials if required. + if auth_required: + _log.debug("Auth required for %s" % url) + if not user: + raise UsageError("authorization required; " + "use auth(user, pass) first") + passwordMgr = _RedirectPasswordMgr() + authHandler = _DigestAuthHandler(passwordMgr) + authHandler.add_password("musicbrainz.org", (), user, password) + handlers.append(authHandler) - opener = urllib2.build_opener(*handlers) + opener = compat.build_opener(*handlers) - # Make request. - req = _MusicbrainzHttpRequest(method, url, data) - req.add_header('User-Agent', _useragent) - _log.debug("requesting with UA %s" % _useragent) - if body: - req.add_header('Content-Type', 'application/xml; charset=UTF-8') - f = _safe_open(opener, req, body) + # Make request. + req = _MusicbrainzHttpRequest(method, url, data) + req.add_header('User-Agent', _useragent) + _log.debug("requesting with UA %s" % _useragent) + if body: + 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) - # Parse the response. - try: - return mbxml.parse_message(f) - except UnicodeError as exc: - raise ResponseError(cause=exc) - except Exception as exc: - if isinstance(exc, ETREE_EXCEPTIONS): - raise ResponseError(cause=exc) - else: - raise + # Parse the response. + try: + return mbxml.parse_message(f) + except UnicodeError as exc: + raise ResponseError(cause=exc) + except Exception as exc: + if isinstance(exc, ETREE_EXCEPTIONS): + raise ResponseError(cause=exc) + else: + raise 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 + """ 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 _do_mb_query(entity, id, includes=[], params={}): - """Make a single GET call to the MusicBrainz XML API. `entity` is a - string indicated the type of object to be retrieved. The id may be - empty, in which case the query is a search. `includes` is a list - of strings that must be valid includes for the entity type. `params` - is a dictionary of additional parameters for the API call. The - response is parsed and returned. - """ - # Build arguments. - _check_includes(entity, includes) - auth_required = _is_auth_required(entity, includes) - args = dict(params) - if len(includes) > 0: - inc = " ".join(includes) - args["inc"] = inc + """Make a single GET call to the MusicBrainz XML API. `entity` is a + string indicated the type of object to be retrieved. The id may be + empty, in which case the query is a search. `includes` is a list + of strings that must be valid includes for the entity type. `params` + is a dictionary of additional parameters for the API call. The + response is parsed and returned. + """ + # Build arguments. + if not isinstance(includes, list): + includes = [includes] + _check_includes(entity, includes) + auth_required = _is_auth_required(entity, includes) + args = dict(params) + if len(includes) > 0: + inc = " ".join(includes) + args["inc"] = inc - # Build the endpoint components. - path = '%s/%s' % (entity, id) - return _mb_request(path, 'GET', auth_required, args=args) + # Build the endpoint components. + path = '%s/%s' % (entity, id) + return _mb_request(path, 'GET', auth_required, args=args) -def _do_mb_search(entity, query='', fields={}, limit=None, offset=None): - """Perform a full-text search on the MusicBrainz search server. - `query` is a free-form query string and `fields` is a dictionary - of key/value query parameters. They keys in `fields` must be valid - for the given entity type. - """ - # Encode the query terms as a Lucene query string. - query_parts = [query.replace('\x00', '').strip()] - for key, value in fields.iteritems(): - # Ensure this is a valid search field. - if key not in VALID_SEARCH_FIELDS[entity]: - raise InvalidSearchFieldError( - '%s is not a valid search field for %s' % (key, entity) - ) +def _do_mb_search(entity, query='', fields={}, + limit=None, offset=None, strict=False): + """Perform a full-text search on the MusicBrainz search server. + `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 + for the given entity type. + """ + # Encode the query terms as a Lucene query string. + query_parts = [] + 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. + if key not in VALID_SEARCH_FIELDS[entity]: + raise InvalidSearchFieldError( + '%s is not a valid search field for %s' % (key, entity) + ) - # Escape Lucene's special characters. - value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', value) - value = value.replace('\x00', '').strip() - value = value.lower() # Avoid binary operators like OR. - if value: - query_parts.append(u'%s:(%s)' % (key, value)) - full_query = u' '.join(query_parts).strip() - if not full_query: - raise ValueError('at least one query term is required') + # Escape Lucene's special characters. + value = util._unicode(value) + value = re.sub(r'([+\-&|!(){}\[\]\^"~*?:\\])', r'\\\1', value) + if value: + if strict: + 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() - # Additional parameters to the search. - params = {'query': full_query} - if limit: - params['limit'] = str(limit) - if offset: - params['offset'] = str(offset) + if not full_query: + raise ValueError('at least one query term is required') - return _do_mb_query(entity, '', [], params) + # Additional parameters to the search. + params = {'query': full_query} + if limit: + params['limit'] = str(limit) + if offset: + params['offset'] = str(offset) + + return _do_mb_query(entity, '', [], params) def _do_mb_delete(path): - """Send a DELETE request for the specified object. - """ - return _mb_request(path, 'DELETE', True, True) + """Send a DELETE request for the specified object. + """ + return _mb_request(path, 'DELETE', True, True) def _do_mb_put(path): - """Send a PUT request for the specified object. - """ - return _mb_request(path, 'PUT', True, True) + """Send a PUT request for the specified object. + """ + return _mb_request(path, 'PUT', True, 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) + """Perform a single POST call for an endpoint with a specified + request body. + """ + return _mb_request(path, 'POST', True, True, body=body) # The main interface! # Single entity by ID def get_artist_by_id(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("artist", id, includes, params) + params = _check_filter_and_make_params("artist", includes, release_status, release_type) + return _do_mb_query("artist", id, includes, params) def get_label_by_id(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("label", id, includes, params) + params = _check_filter_and_make_params("label", includes, release_status, release_type) + return _do_mb_query("label", id, includes, params) def get_recording_by_id(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("recording", id, includes, params) + params = _check_filter_and_make_params("recording", includes, release_status, release_type) + return _do_mb_query("recording", id, includes, params) def get_release_by_id(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("release", id, includes, params) + params = _check_filter_and_make_params("release", includes, release_status, release_type) + return _do_mb_query("release", id, includes, params) def get_release_group_by_id(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("release-group", id, includes, params) + params = _check_filter_and_make_params("release-group", includes, release_status, release_type) + return _do_mb_query("release-group", id, includes, params) def get_work_by_id(id, includes=[]): - return _do_mb_query("work", id, includes) + return _do_mb_query("work", id, includes) # Searching -def search_artists(query='', limit=None, offset=None, **fields): - """Search for artists by a free-form `query` string and/or any of - the following keyword arguments specifying field queries: - arid, artist, sortname, type, begin, end, comment, alias, country, - gender, tag - """ - return _do_mb_search('artist', query, fields, limit, offset) +def search_artists(query='', limit=None, offset=None, strict=False, **fields): + """Search for artists by a free-form `query` string or any of + the following keyword arguments specifying field queries: + arid, artist, sortname, type, begin, end, comment, alias, country, + gender, tag + When `fields` are set, special lucene characters are escaped + in the `query`. + """ + return _do_mb_search('artist', query, fields, limit, offset, strict) -def search_labels(query='', limit=None, offset=None, **fields): - """Search for labels by a free-form `query` string and/or any of - the following keyword arguments specifying field queries: - laid, label, sortname, type, code, country, begin, end, comment, - alias, tag - """ - return _do_mb_search('label', query, fields, limit, offset) +def search_labels(query='', limit=None, offset=None, strict=False, **fields): + """Search for labels by a free-form `query` string or any of + the following keyword arguments specifying field queries: + laid, label, sortname, type, code, country, begin, end, comment, + alias, tag + When `fields` are set, special lucene characters are escaped + in the `query`. + """ + return _do_mb_search('label', query, fields, limit, offset, strict) -def search_recordings(query='', limit=None, offset=None, **fields): - """Search for recordings by a free-form `query` string and/or any of - the following keyword arguments specifying field queries: - rid, recording, isrc, arid, artist, artistname, creditname, reid, - release, type, status, tracks, tracksrelease, dur, qdur, tnum, - position, tag - """ - return _do_mb_search('recording', query, fields, limit, offset) +def search_recordings(query='', limit=None, offset=None, strict=False, **fields): + """Search for recordings by a free-form `query` string or any of + the following keyword arguments specifying field queries: + rid, recording, isrc, arid, artist, artistname, creditname, reid, + release, type, status, tracks, tracksrelease, dur, qdur, tnum, + position, tag + When `fields` are set, special lucene characters are escaped + in the `query`. + """ + return _do_mb_search('recording', query, fields, limit, offset, strict) -def search_releases(query='', limit=None, offset=None, **fields): - """Search for releases by a free-form `query` string and/or any of - the following keyword arguments specifying field queries: - reid, release, arid, artist, artistname, creditname, type, status, - tracks, tracksmedium, discids, discidsmedium, mediums, date, asin, - lang, script, country, date, label, catno, barcode, puid - """ - return _do_mb_search('release', query, fields, limit, offset) +def search_releases(query='', limit=None, offset=None, strict=False, **fields): + """Search for releases by a free-form `query` string or any of + the following keyword arguments specifying field queries: + reid, release, arid, artist, artistname, creditname, type, status, + tracks, tracksmedium, discids, discidsmedium, mediums, date, asin, + 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, strict) -def search_release_groups(query='', limit=None, offset=None, **fields): - """Search for release groups by a free-form `query` string and/or - any of the following keyword arguments specifying field queries: - rgid, releasegroup, reid, release, arid, artist, artistname, - creditname, type, tag - """ - return _do_mb_search('release-group', query, fields, limit, offset) +def search_release_groups(query='', limit=None, offset=None, + strict=False, **fields): + """Search for release groups by a free-form `query` string or + any of the following keyword arguments specifying field queries: + rgid, releasegroup, reid, release, arid, artist, artistname, + 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, strict) -def search_works(query='', limit=None, offset=None, **fields): - """Search for works by a free-form `query` string and/or any of - the following keyword arguments specifying field queries: - wid, work, iswc, type, arid, artist, alias, tag - """ - return _do_mb_search('work', query, fields, limit, offset) +def search_works(query='', limit=None, offset=None, strict=False, **fields): + """Search for works by a free-form `query` string or any of + the following keyword arguments specifying field queries: + 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, strict) # Lists of entities def get_releases_by_discid(id, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type=release_type) - return _do_mb_query("discid", id, includes, params) + params = _check_filter_and_make_params(includes, release_status, release_type=release_type) + return _do_mb_query("discid", id, includes, params) def get_recordings_by_echoprint(echoprint, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("echoprint", echoprint, includes, params) + params = _check_filter_and_make_params(includes, release_status, release_type) + return _do_mb_query("echoprint", echoprint, includes, params) def get_recordings_by_puid(puid, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("puid", puid, includes, params) + params = _check_filter_and_make_params(includes, release_status, release_type) + return _do_mb_query("puid", puid, includes, params) def get_recordings_by_isrc(isrc, includes=[], release_status=[], release_type=[]): - params = _check_filter_and_make_params(includes, release_status, release_type) - return _do_mb_query("isrc", isrc, includes, params) + params = _check_filter_and_make_params(includes, release_status, release_type) + return _do_mb_query("isrc", isrc, includes, params) def get_works_by_iswc(iswc, includes=[]): - return _do_mb_query("iswc", iswc, includes) + return _do_mb_query("iswc", iswc, includes) def _browse_impl(entity, includes, valid_includes, limit, offset, params, release_status=[], release_type=[]): _check_includes_impl(includes, valid_includes) @@ -712,60 +769,74 @@ def browse_release_groups(artist=None, release=None, release_type=[], includes=[ # Collections def get_collections(): - # Missing the count in the reply - return _do_mb_query("collection", '') + # Missing the count in the reply + return _do_mb_query("collection", '') def get_releases_in_collection(collection): - return _do_mb_query("collection", "%s/releases" % collection) + return _do_mb_query("collection", "%s/releases" % collection) # Submission methods def submit_barcodes(barcodes): - """ - Submits a set of {release1: barcode1, release2:barcode2} - Must call auth(user, pass) first - """ - query = mbxml.make_barcode_request(barcodes) - return _do_mb_post("release", query) + """Submits a set of {release1: barcode1, release2:barcode2} + + Must call auth(user, pass) first""" + query = mbxml.make_barcode_request(barcodes) + return _do_mb_post("release", query) def submit_puids(puids): - query = mbxml.make_puid_request(puids) - return _do_mb_post("recording", query) + """Submit PUIDs. + + Must call auth(user, pass) first""" + query = mbxml.make_puid_request(puids) + return _do_mb_post("recording", query) def submit_echoprints(echoprints): - query = mbxml.make_echoprint_request(echoprints) - return _do_mb_post("recording", query) + """Submit echoprints. + + Must call auth(user, pass) first""" + query = mbxml.make_echoprint_request(echoprints) + return _do_mb_post("recording", query) def submit_isrcs(recordings_isrcs): - """ - Submit ISRCs. - Submits a set of {recording-id: [isrc1, irc1]} - Must call auth(user, pass) first - """ + """Submit ISRCs. + Submits a set of {recording-id: [isrc1, isrc1, ...]} + + Must call auth(user, pass) first""" query = mbxml.make_isrc_request(recordings_isrcs=recordings_isrcs) return _do_mb_post("recording", query) def submit_tags(artist_tags={}, recording_tags={}): - """ Submit user tags. - Artist or recording parameters are of the form: - {'entityid': [taglist]} - """ - query = mbxml.make_tag_request(artist_tags, recording_tags) - return _do_mb_post("tag", query) + """Submit user tags. + Artist or recording parameters are of the form: + {'entityid': [taglist]} + + Must call auth(user, pass) first""" + query = mbxml.make_tag_request(artist_tags, recording_tags) + return _do_mb_post("tag", query) def submit_ratings(artist_ratings={}, recording_ratings={}): - """ Submit user ratings. - Artist or recording parameters are of the form: - {'entityid': rating} - """ - query = mbxml.make_rating_request(artist_ratings, recording_ratings) - return _do_mb_post("rating", query) + """ Submit user ratings. + Artist or recording parameters are of the form: + {'entityid': rating} + + Must call auth(user, pass) first""" + query = mbxml.make_rating_request(artist_ratings, recording_ratings) + return _do_mb_post("rating", query) def add_releases_to_collection(collection, releases=[]): - # XXX: Maximum URI length of 16kb means we should only allow ~400 releases - releaselist = ";".join(releases) - _do_mb_put("collection/%s/releases/%s" % (collection, releaselist)) + """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 + releaselist = ";".join(releases) + _do_mb_put("collection/%s/releases/%s" % (collection, releaselist)) def remove_releases_from_collection(collection, releases=[]): - releaselist = ";".join(releases) - _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist)) + """Remove releases from a collection. + Collection and releases should be identified by their MBIDs + + Must call auth(user, pass) first""" + releaselist = ";".join(releases) + _do_mb_delete("collection/%s/releases/%s" % (collection, releaselist)) diff --git a/lib/musicbrainzngs/util.py b/lib/musicbrainzngs/util.py new file mode 100644 index 00000000..efe0e476 --- /dev/null +++ b/lib/musicbrainzngs/util.py @@ -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