From f4e2d1eba03fcd440f414f4898c2ed1ebfdcd3cc Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sat, 26 May 2012 22:38:58 +0200 Subject: [PATCH 01/13] 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 From 214d46493cd6b9bdae0ced55de8e1a7b74ea68e3 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sun, 27 May 2012 09:14:28 +0200 Subject: [PATCH 02/13] Changed findRelease to use the musicbrainzngs library, identical output. (old musicbrainz 2 library is deprecated and broken) --- headphones/mb.py | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 52c48b24..6c5b5d61 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -120,10 +120,10 @@ def findArtist(name, limit=1): def findRelease(name, limit=1): with mb_lock: - - releaselist = [] + limit=25 + releaselistngs = [] attempt = 0 - releaseResults = None + releaseResultsngs = None chars = set('!?') if any((c in chars) for c in name): @@ -134,31 +134,28 @@ def findRelease(name, limit=1): while attempt < 5: try: - releaseResults = q.getReleases(ws.ReleaseFilter(query=name, limit=limit)) + releaseResultsngs = musicbrainzngs.search_releases(query=name,limit=limit)['release-list'] break - except WebServiceError, e: + except WebServiceError, e: #need to update exceptions 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 + if not releaseResultsngs: + return False + for result in releaseResultsngs: + releaselistngs.append({ + 'uniquename': unicode(result['artist-credit'][0]['artist']['name']), + 'title': unicode(result['title']), + 'id': unicode(result['artist-credit'][0]['artist']['id']), + 'albumid': unicode(result['id']), + 'url': unicode("http://musicbrainz.org/artist/" + result['artist-credit'][0]['artist']['id']),#probably needs to be changed + 'albumurl': unicode("http://musicbrainz.org/release/" + result['id']),#probably needs to be changed + 'score': int(result['ext:score']) + }) + return releaselistngs def getArtist(artistid, extrasonly=False): From c8cc29b1abd5e2ab7f27eb55d7be385edefdbb29 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Sun, 27 May 2012 11:44:31 +0200 Subject: [PATCH 03/13] This commit fixes headphones issue 576. Changed getArtist to use the musicbrainzngs library, identical output. (old musicbrainz 2 library is deprecated and broken) --- headphones/mb.py | 81 +++++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 6c5b5d61..a8a33bd6 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -159,12 +159,9 @@ def findRelease(name, limit=1): def getArtist(artistid, extrasonly=False): - with mb_lock: - + 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 @@ -173,37 +170,52 @@ def getArtist(artistid, extrasonly=False): while attempt < 5: try: - artist = q.getArtistById(artistid, inc) + artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status="official",release_type="album")['artist'] 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) - + except Exception,e: + pass + + if not artist: return False time.sleep(sleepytime) + + if 'disambiguation' in artist: + uniquename = unicode(artist['sort-name'] + " (" + artist['disambiguation'] + ")") + else: + uniquename = unicode(artist['sort-name']) + artist_dict['artist_name'] = unicode(artist['name']) + artist_dict['artist_sortname'] = unicode(artist['sort-name']) + artist_dict['artist_uniquename'] = uniquename + artist_dict['artist_type'] = unicode(artist['type']) + + artist_dict['artist_begindate'] = None + artist_dict['artist_enddate'] = None + if 'life-span' in artist: + if 'begin' in artist['life-span']: + artist_dict['artist_begindate'] = unicode(artist['life-span']['begin']) + if 'end' in artist['life-span']: + artist_dict['artist_enddate'] = unicode(artist['life-span']['end']) + - 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(): - + for rg in artist['release-group-list']: + if rg['type'] != 'Album': #only add releases without a secondary type + continue releasegroups.append({ - 'title': rg.title, - 'id': u.extractUuid(rg.id), - 'url': rg.id, - 'type': u.getReleaseTypeName(rg.type) - }) + 'title': unicode(rg['title']), + 'id': unicode(rg['id']), + 'url': u"http://musicbrainz.org/release-group/" + rg['id'], + 'type': unicode(rg['type']) + }) # See if we need to grab extras myDB = db.DBConnection() @@ -214,35 +226,28 @@ def getArtist(artistid, extrasonly=False): 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] + includes = ["single", "ep", "compilation", "soundtrack", "live", "remix"] for include in includes: - inc = ws.ArtistIncludes(releases=(m.Release.TYPE_OFFICIAL, include), releaseGroups=True) - artist = None attempt = 0 - - while attempt < 5: - + while attempt < 5:#this may be redundant with musicbrainzngs, it seems to retry and wait by itself, i will leave it in for rembo to review try: - artist = q.getArtistById(artistid, inc) + artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist'] break - except WebServiceError, e: + except WebServiceError, e:#update exceptions 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(): - + for rg in artist['release-group-list']: releasegroups.append({ - 'title': rg.title, - 'id': u.extractUuid(rg.id), - 'url': rg.id, - 'type': u.getReleaseTypeName(rg.type) - }) - + 'title': unicode(rg['title']), + 'id': unicode(rg['id']), + 'url': u"http://musicbrainz.org/release-group/" + rg['id'], + 'type': unicode(rg['type']) + }) + artist_dict['releasegroups'] = releasegroups return artist_dict From 191d2649515385e922ce6094e6ea4d54f0bfce37 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 00:08:28 +0200 Subject: [PATCH 04/13] getArtist now retrieves all albums, not just the first 25., filtering may have a few problems now, will do a thorough test run tomorrow. --- headphones/mb.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/headphones/mb.py b/headphones/mb.py index a8a33bd6..d02e4ed8 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -170,7 +170,13 @@ def getArtist(artistid, extrasonly=False): while attempt < 5: try: - artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status="official",release_type="album")['artist'] + limit = 100 + artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] + newRgs = None + artist['release-group-list'] = [] + while newRgs == None or len(newRgs) >= limit: + newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list'] + artist['release-group-list'] += newRgs break except WebServiceError, e: logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) From b4b0823f735ff004f37b33ef4228b5cf689698c2 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 09:25:05 +0200 Subject: [PATCH 05/13] Changed getReleaseGroup to use the musicbrainzngs library, compatible but NOT identical output (the musicbrainz database returns slightly different data (additional or missing ASIN etc.) for the v2 protocol, can't do anything about it. (old musicbrainz 2 library is deprecated and broken) --- headphones/mb.py | 111 +++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 56 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index d02e4ed8..28b668c3 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -266,7 +266,6 @@ def getReleaseGroup(rgid): releaselist = [] - inc = ws.ReleaseGroupIncludes(releases=True, artist=True) releaseGroup = None attempt = 0 @@ -275,47 +274,42 @@ def getReleaseGroup(rgid): while attempt < 5: try: - releaseGroup = q.getReleaseGroupById(rgid, inc) + releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group'] 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) + for release in releaseGroup['release-list']: releaseResult = None attempt = 0 - while attempt < 5: - try: - releaseResult = q.getReleaseById(release.id, inc) + releaseResult = musicbrainzngs.get_release_by_id(release['id'],["recordings","media"])['release'] break - except WebServiceError, e: + except WebServiceError, e: #UPDATE THIS logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e))) attempt += 1 - time.sleep(5) - + 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): + if releaseGroup['type'] == 'live' and releaseResult['status'] != 'Official': logger.debug('%s is not an official live album. Skipping' % releaseResult.name) continue - + time.sleep(sleepytime) - + formats = { '2xVinyl': '2', 'Vinyl': '2', @@ -325,68 +319,73 @@ def getReleaseGroup(rgid): 'Digital Media': '0' } - country = { + countries = { 'US': '0', - 'GB': '1', + + 'GB': '1', 'JP': '2', } - - try: - format = int(replace_all(u.extractFragment(releaseResult.releaseEvents[0].format), formats)) + format = int(formats[releaseResult['medium-list'][0]['format']]) except: - format = 3 + format = 3 #this is the same number 'Cassette' uses above, change it ? try: - country = int(replace_all(releaseResult.releaseEvents[0].country, country)) + country = int(countries[releaseResult['country']]) except: country = 3 - + totalTracks = 0 + tracks = [] + for medium in releaseResult['medium-list']: + for track in medium['track-list']: + tracks.append({ + 'number': totalTracks + 1, + 'title': unicode(track['recording']['title']), + 'id': unicode(track['recording']['id']), + 'url': u"http://musicbrainz.org/track/" + track['recording']['id'], + 'duration': int(track['recording']['length']) + }) + totalTracks += 1 + + release_dict = { - 'hasasin': bool(releaseResult.asin), - 'asin': releaseResult.asin, - 'trackscount': len(releaseResult.getTracks()), - 'releaseid': u.extractUuid(releaseResult.id), - 'releasedate': releaseResult.getEarliestReleaseDate(), + 'hasasin': bool(releaseResult.get('asin')), + 'asin': unicode(releaseResult.get('asin')) if 'asin' in releaseResult else None, + 'trackscount': totalTracks, + 'releaseid': unicode(releaseResult.get('id')), + 'releasedate': unicode(releaseResult.get('date')), '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 - + release_dict['tracks'] = tracks releaselist.append(release_dict) + #necessary to make dates that miss the month and/or day show up after full dates + def getSortableReleaseDate(releaseDate): + if releaseDate.count('-') == 2: + return releaseDate + elif releaseDate.count('-') == 1: + return releaseDate + '32' + else: + return releaseDate + '13-32' + + releaselist.sort(key=lambda x:getSortableReleaseDate(x['releasedate'])) + 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'], + 'releasedate' : unicode(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) + 'artist_name' : unicode(releaseGroup['artist-credit'][0]['artist']['name']), + 'artist_id' : unicode(releaseGroup['artist-credit'][0]['artist']['id']), + 'title' : unicode(releaseGroup['title']), + 'type' : unicode(releaseGroup['type']) } return release_dict From d996dd678c233263e4fa96dcc20adb74a7b15c5d Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 11:48:01 +0200 Subject: [PATCH 06/13] Fixed track length problems (modified musicbrainzngs in the process) --- headphones/mb.py | 2 +- lib/musicbrainzngs/mbxml.py | 2 +- lib/musicbrainzngs/musicbrainz.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 28b668c3..9ccebe2d 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -343,7 +343,7 @@ def getReleaseGroup(rgid): 'title': unicode(track['recording']['title']), 'id': unicode(track['recording']['id']), 'url': u"http://musicbrainz.org/track/" + track['recording']['id'], - 'duration': int(track['recording']['length']) + 'duration': int(track['recording']['length'] if 'length' in track['recording'] else track['length'] if 'length' in track else 0) }) totalTracks += 1 diff --git a/lib/musicbrainzngs/mbxml.py b/lib/musicbrainzngs/mbxml.py index c4e46e96..dd4ca961 100644 --- a/lib/musicbrainzngs/mbxml.py +++ b/lib/musicbrainzngs/mbxml.py @@ -430,7 +430,7 @@ def parse_track_list(tl): def parse_track(track): result = {} - elements = ["position", "title"] + elements = ["position", "title","length"] #CHANGED!!! inner_els = {"recording": parse_recording} result.update(parse_elements(elements, track)) diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index 88c54fa7..0fa63b56 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -15,7 +15,7 @@ from lib.musicbrainzngs import mbxml from lib.musicbrainzngs import util from lib.musicbrainzngs import compat -_version = "0.3dev" +_version = "0.3devMODIFIED" _log = logging.getLogger("musicbrainzngs") From a8183ee4dc4f438016c4ae984c579397b903b5db Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 11:52:46 +0200 Subject: [PATCH 07/13] Changed getRelease to use the musicbrainzngs library, compatible but NOT identical output. differences in track durations mostly) (old musicbrainz 2 library is deprecated and broken) --- headphones/mb.py | 60 ++++++++++++++++++++++-------------------------- 1 file changed, 28 insertions(+), 32 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 9ccebe2d..93c380be 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -397,8 +397,6 @@ def getRelease(releaseid): with mb_lock: release = {} - - inc = ws.ReleaseIncludes(tracks=True, releaseEvents=True, releaseGroup=True, artist=True) results = None attempt = 0 @@ -406,10 +404,10 @@ def getRelease(releaseid): while attempt < 5: - try: - results = q.getReleaseById(releaseid, inc) + try: + results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release') break - except WebServiceError, e: + except WebServiceError, e: #update this logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) attempt += 1 time.sleep(5) @@ -418,38 +416,36 @@ def getRelease(releaseid): return False time.sleep(sleepytime) - - release['title'] = results.title - release['id'] = u.extractUuid(results.id) - release['asin'] = results.asin - release['date'] = results.getEarliestReleaseDate() + release['title'] = unicode(results['title']) + release['id'] = unicode(results['id']) + release['asin'] = unicode(results['asin']) if 'asin' in results else None + release['date'] = unicode(results['date']) - rg = results.getReleaseGroup() - if rg: - release['rgid'] = u.extractUuid(rg.id) - release['rg_title'] = rg.title - release['rg_type'] = u.extractFragment(rg.type) + if 'release-group' in results: + release['rgid'] = unicode(results['release-group']['id']) + release['rg_title'] = unicode(results['release-group']['title']) + release['rg_type'] = unicode(results['release-group']['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) - + + release['artist_name'] = unicode(results['artist-credit'][0]['artist']['name']) + release['artist_id'] = unicode(results['artist-credit'][0]['artist']['id']) + + + totalTracks = 0 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 - + for medium in results['medium-list']: + for track in medium['track-list']: + tracks.append({ + 'number': totalTracks + 1, + 'title': unicode(track['recording']['title']), + 'id': unicode(track['recording']['id']), + 'url': u"http://musicbrainz.org/track/" + track['recording']['id'], + 'duration': int(track['length']) if 'length' in track else 0 + }) + totalTracks += 1 + release['tracks'] = tracks return release From 0f0fffbb9f9efb062eee1b9483c5fc546895bced Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 15:11:24 +0200 Subject: [PATCH 08/13] Changed findArtistbyAlbum to use the musicbrainzngs library, compatible but NOT identical output. differences seem to be restricted to the uniquename entry in the artist_dict) --- headphones/mb.py | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 93c380be..a8999e18 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -463,39 +463,42 @@ def findArtistbyAlbum(name): # 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) + results = musicbrainzngs.search_release_groups(term).get('release-group-list') break - except WebServiceError, e: + except WebServiceError, e: #update exceptions logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) attempt += 1 time.sleep(10) time.sleep(sleepytime) - if not results: + if not results: return False artist_dict = {} + for releaseGroup in results: + newArtist = releaseGroup['artist-credit'][0]['artist'] + if 'disambiguation' in newArtist: + uniquename = unicode(newArtist['sort-name'] + " (" + newArtist['disambiguation'] + ")") + else: + uniquename = unicode(newArtist['sort-name']) + artist_dict['name'] = unicode(newArtist['sort-name']) + artist_dict['uniquename'] = uniquename + artist_dict['id'] = unicode(newArtist['id']) + artist_dict['url'] = u'http://musicbrainz.org/artist/' + newArtist['id'] + artist_dict['score'] = int(releaseGroup['ext:score']) + - 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 From 242ee9e5b55eab02a9eae7b504893a950d6f3065 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 15:40:36 +0200 Subject: [PATCH 09/13] Changed findAlbumID to use the musicbrainzngs library, identical output. --- headphones/mb.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index a8999e18..6a17c24b 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -504,8 +504,7 @@ def findArtistbyAlbum(name): def findAlbumID(artist=None, album=None): - f = ws.ReleaseGroupFilter(title=album, artistName=artist, limit=1) - results = None + results_ngs = None attempt = 0 q, sleepytime = startmb(forcemb=True) @@ -513,17 +512,20 @@ def findAlbumID(artist=None, album=None): while attempt < 5: try: - results = q.getReleaseGroups(f) + term = '"'+album+'" AND artist:"'+artist+'"' + results_ngs = musicbrainzngs.search_release_groups(term,1).get('release-group-list') break - except WebServiceError, e: + except WebServiceError, e:#update exceptions logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) attempt += 1 - time.sleep(10) + time.sleep(10) time.sleep(sleepytime) - if not results: + if not results_ngs: return False - - rgid = u.extractUuid(results[0].releaseGroup.id) - return rgid + + if len(results_ngs) < 1: + return False + rgid_ngs = unicode(results_ngs[0]['id']) + return rgid_ngs From 75d68d12d787c125a2fbcb35a8c370c2aa32cbde Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 16:47:12 +0200 Subject: [PATCH 10/13] Added authentication (i think) and removed some debug stuff --- headphones/mb.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 6a17c24b..15f6ed58 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -49,9 +49,16 @@ def startmb(forcemb=False): 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") + if sleepytime == 0: + musicbrainzngs.set_rate_limit(False) + else: + musicbrainzngs.set_rate_limit(True) + + #CHECK THIS + if mbuser and mbpass:#i have no idea if this will work or not + musicbrainzngs.auth(mbuser,mbpass) + #CHECK THIS #q = musicbrainzngs service = ws.WebService(host=mbhost, port=mbport, username=mbuser, password=mbpass, mirror=headphones.MIRROR) @@ -63,8 +70,7 @@ def startmb(forcemb=False): def findArtist(name, limit=1): - with mb_lock: - limit = 25 + with mb_lock: artistlist = [] attempt = 0 artistResults = None @@ -119,8 +125,7 @@ def findArtist(name, limit=1): def findRelease(name, limit=1): - with mb_lock: - limit=25 + with mb_lock: releaselistngs = [] attempt = 0 releaseResultsngs = None @@ -346,7 +351,6 @@ def getReleaseGroup(rgid): 'duration': int(track['recording']['length'] if 'length' in track['recording'] else track['length'] if 'length' in track else 0) }) totalTracks += 1 - release_dict = { 'hasasin': bool(releaseResult.get('asin')), @@ -509,8 +513,7 @@ def findAlbumID(artist=None, album=None): q, sleepytime = startmb(forcemb=True) - while attempt < 5: - + while attempt < 5: try: term = '"'+album+'" AND artist:"'+artist+'"' results_ngs = musicbrainzngs.search_release_groups(term,1).get('release-group-list') From 85b08cfc05402f862babf73ec13455a8ee208819 Mon Sep 17 00:00:00 2001 From: Patrick Speiser Date: Mon, 28 May 2012 17:01:45 +0200 Subject: [PATCH 11/13] Replaced musicbrainz2 with musicbrainzngs. Authentication may or may not work (i do not have access to the headphones server). Fixes Issue #576 and Issue #624 --- headphones/mb.py | 11 +- lib/musicbrainz2/__init__.py | 26 - lib/musicbrainz2/data/__init__.py | 10 - lib/musicbrainz2/data/countrynames.py | 253 --- lib/musicbrainz2/data/languagenames.py | 400 ---- lib/musicbrainz2/data/releasetypenames.py | 24 - lib/musicbrainz2/data/scriptnames.py | 59 - lib/musicbrainz2/disc.py | 221 -- lib/musicbrainz2/model.py | 2488 --------------------- lib/musicbrainz2/utils.py | 204 -- lib/musicbrainz2/webservice.py | 1524 ------------- lib/musicbrainz2/wsxml.py | 1675 -------------- 12 files changed, 2 insertions(+), 6893 deletions(-) delete mode 100644 lib/musicbrainz2/__init__.py delete mode 100644 lib/musicbrainz2/data/__init__.py delete mode 100644 lib/musicbrainz2/data/countrynames.py delete mode 100644 lib/musicbrainz2/data/languagenames.py delete mode 100644 lib/musicbrainz2/data/releasetypenames.py delete mode 100644 lib/musicbrainz2/data/scriptnames.py delete mode 100644 lib/musicbrainz2/disc.py delete mode 100644 lib/musicbrainz2/model.py delete mode 100644 lib/musicbrainz2/utils.py delete mode 100644 lib/musicbrainz2/webservice.py delete mode 100644 lib/musicbrainz2/wsxml.py diff --git a/headphones/mb.py b/headphones/mb.py index 15f6ed58..c4072d51 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -3,12 +3,6 @@ from __future__ import with_statement import time import threading -import lib.musicbrainz2.webservice as ws -import lib.musicbrainz2.model as m -import lib.musicbrainz2.utils as u - -from lib.musicbrainz2.webservice import WebServiceError - import headphones from headphones import logger, db from headphones.helpers import multikeysort, replace_all @@ -61,8 +55,7 @@ def startmb(forcemb=False): #CHECK THIS #q = musicbrainzngs - service = ws.WebService(host=mbhost, port=mbport, username=mbuser, password=mbpass, mirror=headphones.MIRROR) - q = ws.Query(service) + q = musicbrainzngs logger.debug('Using the following server values:\nMBHost: %s ; MBPort: %i ; Sleep Interval: %i ' % (mbhost, mbport, sleepytime)) @@ -82,7 +75,7 @@ def findArtist(name, limit=1): q, sleepytime = startmb(forcemb=True) while attempt < 5: - try: + try: artistResults = musicbrainzngs.search_artists(query=name,limit=limit)['artist-list'] break except WebServiceError, e:#need to update the exceptions diff --git a/lib/musicbrainz2/__init__.py b/lib/musicbrainz2/__init__.py deleted file mode 100644 index f2edb508..00000000 --- a/lib/musicbrainz2/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -"""A collection of classes for MusicBrainz. - -To get started quickly, have a look at L{webservice.Query} and the examples -there. The source distribution also contains example code you might find -interesting. - -This package contains the following modules: - - 1. L{model}: The MusicBrainz domain model, containing classes like - L{Artist }, L{Release }, or - L{Track } - - 2. L{webservice}: An interface to the MusicBrainz XML web service. - - 3. L{wsxml}: A parser for the web service XML format (MMD). - - 4. L{disc}: Functions for creating and submitting DiscIDs. - - 5. L{utils}: Utilities for working with URIs and other commonly needed tools. - -@author: Matthias Friedrich -""" -__revision__ = '$Id: __init__.py 12974 2011-05-01 08:43:54Z luks $' -__version__ = '0.7.3' - -# EOF diff --git a/lib/musicbrainz2/data/__init__.py b/lib/musicbrainz2/data/__init__.py deleted file mode 100644 index 3067fabc..00000000 --- a/lib/musicbrainz2/data/__init__.py +++ /dev/null @@ -1,10 +0,0 @@ -"""Support data for the musicbrainz2 package. - -This package is I{not} part of the public API, it has been added to work -around shortcomings in python and may thus be removed at any time. - -Please use the L{musicbrainz2.utils} module instead. -""" -__revision__ = '$Id: __init__.py 7386 2006-04-30 11:12:55Z matt $' - -# EOF diff --git a/lib/musicbrainz2/data/countrynames.py b/lib/musicbrainz2/data/countrynames.py deleted file mode 100644 index 7c4ab023..00000000 --- a/lib/musicbrainz2/data/countrynames.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- - -__revision__ = '$Id: countrynames.py 7386 2006-04-30 11:12:55Z matt $' - -countryNames = { - u'BD': u'Bangladesh', - u'BE': u'Belgium', - u'BF': u'Burkina Faso', - u'BG': u'Bulgaria', - u'BB': u'Barbados', - u'WF': u'Wallis and Futuna Islands', - u'BM': u'Bermuda', - u'BN': u'Brunei Darussalam', - u'BO': u'Bolivia', - u'BH': u'Bahrain', - u'BI': u'Burundi', - u'BJ': u'Benin', - u'BT': u'Bhutan', - u'JM': u'Jamaica', - u'BV': u'Bouvet Island', - u'BW': u'Botswana', - u'WS': u'Samoa', - u'BR': u'Brazil', - u'BS': u'Bahamas', - u'BY': u'Belarus', - u'BZ': u'Belize', - u'RU': u'Russian Federation', - u'RW': u'Rwanda', - u'RE': u'Reunion', - u'TM': u'Turkmenistan', - u'TJ': u'Tajikistan', - u'RO': u'Romania', - u'TK': u'Tokelau', - u'GW': u'Guinea-Bissau', - u'GU': u'Guam', - u'GT': u'Guatemala', - u'GR': u'Greece', - u'GQ': u'Equatorial Guinea', - u'GP': u'Guadeloupe', - u'JP': u'Japan', - u'GY': u'Guyana', - u'GF': u'French Guiana', - u'GE': u'Georgia', - u'GD': u'Grenada', - u'GB': u'United Kingdom', - u'GA': u'Gabon', - u'SV': u'El Salvador', - u'GN': u'Guinea', - u'GM': u'Gambia', - u'GL': u'Greenland', - u'GI': u'Gibraltar', - u'GH': u'Ghana', - u'OM': u'Oman', - u'TN': u'Tunisia', - u'JO': u'Jordan', - u'HT': u'Haiti', - u'HU': u'Hungary', - u'HK': u'Hong Kong', - u'HN': u'Honduras', - u'HM': u'Heard and Mc Donald Islands', - u'VE': u'Venezuela', - u'PR': u'Puerto Rico', - u'PW': u'Palau', - u'PT': u'Portugal', - u'SJ': u'Svalbard and Jan Mayen Islands', - u'PY': u'Paraguay', - u'IQ': u'Iraq', - u'PA': u'Panama', - u'PF': u'French Polynesia', - u'PG': u'Papua New Guinea', - u'PE': u'Peru', - u'PK': u'Pakistan', - u'PH': u'Philippines', - u'PN': u'Pitcairn', - u'PL': u'Poland', - u'PM': u'St. Pierre and Miquelon', - u'ZM': u'Zambia', - u'EH': u'Western Sahara', - u'EE': u'Estonia', - u'EG': u'Egypt', - u'ZA': u'South Africa', - u'EC': u'Ecuador', - u'IT': u'Italy', - u'VN': u'Viet Nam', - u'SB': u'Solomon Islands', - u'ET': u'Ethiopia', - u'SO': u'Somalia', - u'ZW': u'Zimbabwe', - u'SA': u'Saudi Arabia', - u'ES': u'Spain', - u'ER': u'Eritrea', - u'MD': u'Moldova, Republic of', - u'MG': u'Madagascar', - u'MA': u'Morocco', - u'MC': u'Monaco', - u'UZ': u'Uzbekistan', - u'MM': u'Myanmar', - u'ML': u'Mali', - u'MO': u'Macau', - u'MN': u'Mongolia', - u'MH': u'Marshall Islands', - u'MK': u'Macedonia, The Former Yugoslav Republic of', - u'MU': u'Mauritius', - u'MT': u'Malta', - u'MW': u'Malawi', - u'MV': u'Maldives', - u'MQ': u'Martinique', - u'MP': u'Northern Mariana Islands', - u'MS': u'Montserrat', - u'MR': u'Mauritania', - u'UG': u'Uganda', - u'MY': u'Malaysia', - u'MX': u'Mexico', - u'IL': u'Israel', - u'FR': u'France', - u'IO': u'British Indian Ocean Territory', - u'SH': u'St. Helena', - u'FI': u'Finland', - u'FJ': u'Fiji', - u'FK': u'Falkland Islands (Malvinas)', - u'FM': u'Micronesia, Federated States of', - u'FO': u'Faroe Islands', - u'NI': u'Nicaragua', - u'NL': u'Netherlands', - u'NO': u'Norway', - u'NA': u'Namibia', - u'VU': u'Vanuatu', - u'NC': u'New Caledonia', - u'NE': u'Niger', - u'NF': u'Norfolk Island', - u'NG': u'Nigeria', - u'NZ': u'New Zealand', - u'ZR': u'Zaire', - u'NP': u'Nepal', - u'NR': u'Nauru', - u'NU': u'Niue', - u'CK': u'Cook Islands', - u'CI': u'Cote d\'Ivoire', - u'CH': u'Switzerland', - u'CO': u'Colombia', - u'CN': u'China', - u'CM': u'Cameroon', - u'CL': u'Chile', - u'CC': u'Cocos (Keeling) Islands', - u'CA': u'Canada', - u'CG': u'Congo', - u'CF': u'Central African Republic', - u'CZ': u'Czech Republic', - u'CY': u'Cyprus', - u'CX': u'Christmas Island', - u'CR': u'Costa Rica', - u'CV': u'Cape Verde', - u'CU': u'Cuba', - u'SZ': u'Swaziland', - u'SY': u'Syrian Arab Republic', - u'KG': u'Kyrgyzstan', - u'KE': u'Kenya', - u'SR': u'Suriname', - u'KI': u'Kiribati', - u'KH': u'Cambodia', - u'KN': u'Saint Kitts and Nevis', - u'KM': u'Comoros', - u'ST': u'Sao Tome and Principe', - u'SI': u'Slovenia', - u'KW': u'Kuwait', - u'SN': u'Senegal', - u'SM': u'San Marino', - u'SL': u'Sierra Leone', - u'SC': u'Seychelles', - u'KZ': u'Kazakhstan', - u'KY': u'Cayman Islands', - u'SG': u'Singapore', - u'SE': u'Sweden', - u'SD': u'Sudan', - u'DO': u'Dominican Republic', - u'DM': u'Dominica', - u'DJ': u'Djibouti', - u'DK': u'Denmark', - u'VG': u'Virgin Islands (British)', - u'DE': u'Germany', - u'YE': u'Yemen', - u'DZ': u'Algeria', - u'US': u'United States', - u'UY': u'Uruguay', - u'YT': u'Mayotte', - u'UM': u'United States Minor Outlying Islands', - u'LB': u'Lebanon', - u'LC': u'Saint Lucia', - u'LA': u'Lao People\'s Democratic Republic', - u'TV': u'Tuvalu', - u'TW': u'Taiwan', - u'TT': u'Trinidad and Tobago', - u'TR': u'Turkey', - u'LK': u'Sri Lanka', - u'LI': u'Liechtenstein', - u'LV': u'Latvia', - u'TO': u'Tonga', - u'LT': u'Lithuania', - u'LU': u'Luxembourg', - u'LR': u'Liberia', - u'LS': u'Lesotho', - u'TH': u'Thailand', - u'TF': u'French Southern Territories', - u'TG': u'Togo', - u'TD': u'Chad', - u'TC': u'Turks and Caicos Islands', - u'LY': u'Libyan Arab Jamahiriya', - u'VA': u'Vatican City State (Holy See)', - u'VC': u'Saint Vincent and The Grenadines', - u'AE': u'United Arab Emirates', - u'AD': u'Andorra', - u'AG': u'Antigua and Barbuda', - u'AF': u'Afghanistan', - u'AI': u'Anguilla', - u'VI': u'Virgin Islands (U.S.)', - u'IS': u'Iceland', - u'IR': u'Iran (Islamic Republic of)', - u'AM': u'Armenia', - u'AL': u'Albania', - u'AO': u'Angola', - u'AN': u'Netherlands Antilles', - u'AQ': u'Antarctica', - u'AS': u'American Samoa', - u'AR': u'Argentina', - u'AU': u'Australia', - u'AT': u'Austria', - u'AW': u'Aruba', - u'IN': u'India', - u'TZ': u'Tanzania, United Republic of', - u'AZ': u'Azerbaijan', - u'IE': u'Ireland', - u'ID': u'Indonesia', - u'UA': u'Ukraine', - u'QA': u'Qatar', - u'MZ': u'Mozambique', - u'BA': u'Bosnia and Herzegovina', - u'CD': u'Congo, The Democratic Republic of the', - u'CS': u'Serbia and Montenegro', - u'HR': u'Croatia', - u'KP': u'Korea (North), Democratic People\'s Republic of', - u'KR': u'Korea (South), Republic of', - u'SK': u'Slovakia', - u'SU': u'Soviet Union (historical, 1922-1991)', - u'TL': u'East Timor', - u'XC': u'Czechoslovakia (historical, 1918-1992)', - u'XE': u'Europe', - u'XG': u'East Germany (historical, 1949-1990)', - u'XU': u'[Unknown Country]', - u'XW': u'[Worldwide]', - u'YU': u'Yugoslavia (historical, 1918-1992)', -} - -# EOF diff --git a/lib/musicbrainz2/data/languagenames.py b/lib/musicbrainz2/data/languagenames.py deleted file mode 100644 index 7f4252dc..00000000 --- a/lib/musicbrainz2/data/languagenames.py +++ /dev/null @@ -1,400 +0,0 @@ -# -*- coding: utf-8 -*- - -__revision__ = '$Id: languagenames.py 8725 2006-12-17 22:39:07Z luks $' - -languageNames = { - u'ART': u'Artificial (Other)', - u'ROH': u'Raeto-Romance', - u'SCO': u'Scots', - u'SCN': u'Sicilian', - u'ROM': u'Romany', - u'RON': u'Romanian', - u'OSS': u'Ossetian; Ossetic', - u'ALE': u'Aleut', - u'MNI': u'Manipuri', - u'NWC': u'Classical Newari; Old Newari; Classical Nepal Bhasa', - u'OSA': u'Osage', - u'MNC': u'Manchu', - u'MWR': u'Marwari', - u'VEN': u'Venda', - u'MWL': u'Mirandese', - u'FAS': u'Persian', - u'FAT': u'Fanti', - u'FAN': u'Fang', - u'FAO': u'Faroese', - u'DIN': u'Dinka', - u'HYE': u'Armenian', - u'DSB': u'Lower Sorbian', - u'CAR': u'Carib', - u'DIV': u'Divehi', - u'TEL': u'Telugu', - u'TEM': u'Timne', - u'NBL': u'Ndebele, South; South Ndebele', - u'TER': u'Tereno', - u'TET': u'Tetum', - u'SUN': u'Sundanese', - u'KUT': u'Kutenai', - u'SUK': u'Sukuma', - u'KUR': u'Kurdish', - u'KUM': u'Kumyk', - u'SUS': u'Susu', - u'NEW': u'Newari; Nepal Bhasa', - u'KUA': u'Kuanyama; Kwanyama', - u'MEN': u'Mende', - u'LEZ': u'Lezghian', - u'GLA': u'Gaelic; Scottish Gaelic', - u'BOS': u'Bosnian', - u'GLE': u'Irish', - u'EKA': u'Ekajuk', - u'GLG': u'Gallegan', - u'AKA': u'Akan', - u'BOD': u'Tibetan', - u'GLV': u'Manx', - u'JRB': u'Judeo-Arabic', - u'VIE': u'Vietnamese', - u'IPK': u'Inupiaq', - u'UZB': u'Uzbek', - u'BRE': u'Breton', - u'BRA': u'Braj', - u'AYM': u'Aymara', - u'CHA': u'Chamorro', - u'CHB': u'Chibcha', - u'CHE': u'Chechen', - u'CHG': u'Chagatai', - u'CHK': u'Chuukese', - u'CHM': u'Mari', - u'CHN': u'Chinook jargon', - u'CHO': u'Choctaw', - u'CHP': u'Chipewyan', - u'CHR': u'Cherokee', - u'CHU': u'Church Slavic; Old Slavonic; Church Slavonic; Old Bulgarian; Old Church Slavonic', - u'CHV': u'Chuvash', - u'CHY': u'Cheyenne', - u'MSA': u'Malay', - u'III': u'Sichuan Yi', - u'ACE': u'Achinese', - u'IBO': u'Igbo', - u'IBA': u'Iban', - u'XHO': u'Xhosa', - u'DEU': u'German', - u'CAT': u'Catalan; Valencian', - u'DEL': u'Delaware', - u'DEN': u'Slave (Athapascan)', - u'CAD': u'Caddo', - u'TAT': u'Tatar', - u'RAJ': u'Rajasthani', - u'SPA': u'Spanish; Castilian', - u'TAM': u'Tamil', - u'TAH': u'Tahitian', - u'AFH': u'Afrihili', - u'ENG': u'English', - u'CSB': u'Kashubian', - u'NYN': u'Nyankole', - u'NYO': u'Nyoro', - u'SID': u'Sidamo', - u'NYA': u'Chichewa; Chewa; Nyanja', - u'SIN': u'Sinhala; Sinhalese', - u'AFR': u'Afrikaans', - u'LAM': u'Lamba', - u'SND': u'Sindhi', - u'MAR': u'Marathi', - u'LAH': u'Lahnda', - u'NYM': u'Nyamwezi', - u'SNA': u'Shona', - u'LAD': u'Ladino', - u'SNK': u'Soninke', - u'MAD': u'Madurese', - u'MAG': u'Magahi', - u'MAI': u'Maithili', - u'MAH': u'Marshallese', - u'LAV': u'Latvian', - u'MAL': u'Malayalam', - u'MAN': u'Mandingo', - u'ZND': u'Zande', - u'ZEN': u'Zenaga', - u'KBD': u'Kabardian', - u'ITA': u'Italian', - u'VAI': u'Vai', - u'TSN': u'Tswana', - u'TSO': u'Tsonga', - u'TSI': u'Tsimshian', - u'BYN': u'Blin; Bilin', - u'FIJ': u'Fijian', - u'FIN': u'Finnish', - u'EUS': u'Basque', - u'CEB': u'Cebuano', - u'DAN': u'Danish', - u'NOG': u'Nogai', - u'NOB': u'Norwegian Bokmål; Bokmål, Norwegian', - u'DAK': u'Dakota', - u'CES': u'Czech', - u'DAR': u'Dargwa', - u'DAY': u'Dayak', - u'NOR': u'Norwegian', - u'KPE': u'Kpelle', - u'GUJ': u'Gujarati', - u'MDF': u'Moksha', - u'MAS': u'Masai', - u'LAO': u'Lao', - u'MDR': u'Mandar', - u'GON': u'Gondi', - u'SMS': u'Skolt Sami', - u'SMO': u'Samoan', - u'SMN': u'Inari Sami', - u'SMJ': u'Lule Sami', - u'GOT': u'Gothic', - u'SME': u'Northern Sami', - u'BLA': u'Siksika', - u'SMA': u'Southern Sami', - u'GOR': u'Gorontalo', - u'AST': u'Asturian; Bable', - u'ORM': u'Oromo', - u'QUE': u'Quechua', - u'ORI': u'Oriya', - u'CRH': u'Crimean Tatar; Crimean Turkish', - u'ASM': u'Assamese', - u'PUS': u'Pushto', - u'DGR': u'Dogrib', - u'LTZ': u'Luxembourgish; Letzeburgesch', - u'NDO': u'Ndonga', - u'GEZ': u'Geez', - u'ISL': u'Icelandic', - u'LAT': u'Latin', - u'MAK': u'Makasar', - u'ZAP': u'Zapotec', - u'YID': u'Yiddish', - u'KOK': u'Konkani', - u'KOM': u'Komi', - u'KON': u'Kongo', - u'UKR': u'Ukrainian', - u'TON': u'Tonga (Tonga Islands)', - u'KOS': u'Kosraean', - u'KOR': u'Korean', - u'TOG': u'Tonga (Nyasa)', - u'HUN': u'Hungarian', - u'HUP': u'Hupa', - u'CYM': u'Welsh', - u'UDM': u'Udmurt', - u'BEJ': u'Beja', - u'BEN': u'Bengali', - u'BEL': u'Belarusian', - u'BEM': u'Bemba', - u'AAR': u'Afar', - u'NZI': u'Nzima', - u'SAH': u'Yakut', - u'SAN': u'Sanskrit', - u'SAM': u'Samaritan Aramaic', - u'SAG': u'Sango', - u'SAD': u'Sandawe', - u'RAR': u'Rarotongan', - u'RAP': u'Rapanui', - u'SAS': u'Sasak', - u'SAT': u'Santali', - u'MIN': u'Minangkabau', - u'LIM': u'Limburgan; Limburger; Limburgish', - u'LIN': u'Lingala', - u'LIT': u'Lithuanian', - u'EFI': u'Efik', - u'BTK': u'Batak (Indonesia)', - u'KAC': u'Kachin', - u'KAB': u'Kabyle', - u'KAA': u'Kara-Kalpak', - u'KAN': u'Kannada', - u'KAM': u'Kamba', - u'KAL': u'Kalaallisut; Greenlandic', - u'KAS': u'Kashmiri', - u'KAR': u'Karen', - u'KAU': u'Kanuri', - u'KAT': u'Georgian', - u'KAZ': u'Kazakh', - u'TYV': u'Tuvinian', - u'AWA': u'Awadhi', - u'URD': u'Urdu', - u'DOI': u'Dogri', - u'TPI': u'Tok Pisin', - u'MRI': u'Maori', - u'ABK': u'Abkhazian', - u'TKL': u'Tokelau', - u'NLD': u'Dutch; Flemish', - u'OJI': u'Ojibwa', - u'OCI': u'Occitan (post 1500); Provençal', - u'WOL': u'Wolof', - u'JAV': u'Javanese', - u'HRV': u'Croatian', - u'DYU': u'Dyula', - u'SSW': u'Swati', - u'MUL': u'Multiple languages', - u'HIL': u'Hiligaynon', - u'HIM': u'Himachali', - u'HIN': u'Hindi', - u'BAS': u'Basa', - u'GBA': u'Gbaya', - u'WLN': u'Walloon', - u'BAD': u'Banda', - u'NEP': u'Nepali', - u'CRE': u'Cree', - u'BAN': u'Balinese', - u'BAL': u'Baluchi', - u'BAM': u'Bambara', - u'BAK': u'Bashkir', - u'SHN': u'Shan', - u'ARP': u'Arapaho', - u'ARW': u'Arawak', - u'ARA': u'Arabic', - u'ARC': u'Aramaic', - u'ARG': u'Aragonese', - u'SEL': u'Selkup', - u'ARN': u'Araucanian', - u'LUS': u'Lushai', - u'MUS': u'Creek', - u'LUA': u'Luba-Lulua', - u'LUB': u'Luba-Katanga', - u'LUG': u'Ganda', - u'LUI': u'Luiseno', - u'LUN': u'Lunda', - u'LUO': u'Luo (Kenya and Tanzania)', - u'IKU': u'Inuktitut', - u'TUR': u'Turkish', - u'TUK': u'Turkmen', - u'TUM': u'Tumbuka', - u'COP': u'Coptic', - u'COS': u'Corsican', - u'COR': u'Cornish', - u'ILO': u'Iloko', - u'GWI': u'Gwich´in', - u'TLI': u'Tlingit', - u'TLH': u'Klingon; tlhIngan-Hol', - u'POR': u'Portuguese', - u'PON': u'Pohnpeian', - u'POL': u'Polish', - u'TGK': u'Tajik', - u'TGL': u'Tagalog', - u'FRA': u'French', - u'BHO': u'Bhojpuri', - u'SWA': u'Swahili', - u'DUA': u'Duala', - u'SWE': u'Swedish', - u'YAP': u'Yapese', - u'TIV': u'Tiv', - u'YAO': u'Yao', - u'XAL': u'Kalmyk', - u'FRY': u'Frisian', - u'GAY': u'Gayo', - u'OTA': u'Turkish, Ottoman (1500-1928)', - u'HMN': u'Hmong', - u'HMO': u'Hiri Motu', - u'GAA': u'Ga', - u'FUR': u'Friulian', - u'MLG': u'Malagasy', - u'SLV': u'Slovenian', - u'FIL': u'Filipino; Pilipino', - u'MLT': u'Maltese', - u'SLK': u'Slovak', - u'FUL': u'Fulah', - u'JPN': u'Japanese', - u'VOL': u'Volapük', - u'VOT': u'Votic', - u'IND': u'Indonesian', - u'AVE': u'Avestan', - u'JPR': u'Judeo-Persian', - u'AVA': u'Avaric', - u'PAP': u'Papiamento', - u'EWO': u'Ewondo', - u'PAU': u'Palauan', - u'EWE': u'Ewe', - u'PAG': u'Pangasinan', - u'PAM': u'Pampanga', - u'PAN': u'Panjabi; Punjabi', - u'KIR': u'Kirghiz', - u'NIA': u'Nias', - u'KIK': u'Kikuyu; Gikuyu', - u'SYR': u'Syriac', - u'KIN': u'Kinyarwanda', - u'NIU': u'Niuean', - u'EPO': u'Esperanto', - u'JBO': u'Lojban', - u'MIC': u'Mi\'kmaq; Micmac', - u'THA': u'Thai', - u'HAI': u'Haida', - u'ELL': u'Greek, Modern (1453-)', - u'ADY': u'Adyghe; Adygei', - u'ELX': u'Elamite', - u'ADA': u'Adangme', - u'GRB': u'Grebo', - u'HAT': u'Haitian; Haitian Creole', - u'HAU': u'Hausa', - u'HAW': u'Hawaiian', - u'BIN': u'Bini', - u'AMH': u'Amharic', - u'BIK': u'Bikol', - u'BIH': u'Bihari', - u'MOS': u'Mossi', - u'MOH': u'Mohawk', - u'MON': u'Mongolian', - u'MOL': u'Moldavian', - u'BIS': u'Bislama', - u'TVL': u'Tuvalu', - u'IJO': u'Ijo', - u'EST': u'Estonian', - u'KMB': u'Kimbundu', - u'UMB': u'Umbundu', - u'TMH': u'Tamashek', - u'FON': u'Fon', - u'HSB': u'Upper Sorbian', - u'RUN': u'Rundi', - u'RUS': u'Russian', - u'PLI': u'Pali', - u'SRD': u'Sardinian', - u'ACH': u'Acoli', - u'NDE': u'Ndebele, North; North Ndebele', - u'DZO': u'Dzongkha', - u'KRU': u'Kurukh', - u'SRR': u'Serer', - u'IDO': u'Ido', - u'SRP': u'Serbian', - u'KRO': u'Kru', - u'KRC': u'Karachay-Balkar', - u'NDS': u'Low German; Low Saxon; German, Low; Saxon, Low', - u'ZUN': u'Zuni', - u'ZUL': u'Zulu', - u'TWI': u'Twi', - u'NSO': u'Northern Sotho, Pedi; Sepedi', - u'SOM': u'Somali', - u'SON': u'Songhai', - u'SOT': u'Sotho, Southern', - u'MKD': u'Macedonian', - u'HER': u'Herero', - u'LOL': u'Mongo', - u'HEB': u'Hebrew', - u'LOZ': u'Lozi', - u'GIL': u'Gilbertese', - u'WAS': u'Washo', - u'WAR': u'Waray', - u'BUL': u'Bulgarian', - u'WAL': u'Walamo', - u'BUA': u'Buriat', - u'BUG': u'Buginese', - u'AZE': u'Azerbaijani', - u'ZHA': u'Zhuang; Chuang', - u'ZHO': u'Chinese', - u'NNO': u'Norwegian Nynorsk; Nynorsk, Norwegian', - u'UIG': u'Uighur; Uyghur', - u'MYV': u'Erzya', - u'INH': u'Ingush', - u'KHM': u'Khmer', - u'MYA': u'Burmese', - u'KHA': u'Khasi', - u'INA': u'Interlingua (International Auxiliary Language Association)', - u'NAH': u'Nahuatl', - u'TIR': u'Tigrinya', - u'NAP': u'Neapolitan', - u'NAV': u'Navajo; Navaho', - u'NAU': u'Nauru', - u'GRN': u'Guarani', - u'TIG': u'Tigre', - u'YOR': u'Yoruba', - u'ILE': u'Interlingue', - u'SQI': u'Albanian', -} - -# EOF diff --git a/lib/musicbrainz2/data/releasetypenames.py b/lib/musicbrainz2/data/releasetypenames.py deleted file mode 100644 index f16ed19e..00000000 --- a/lib/musicbrainz2/data/releasetypenames.py +++ /dev/null @@ -1,24 +0,0 @@ -# -*- coding: utf-8 -*- - -__revision__ = '$Id: releasetypenames.py 8728 2006-12-17 23:42:30Z luks $' - -releaseTypeNames = { - u'http://musicbrainz.org/ns/mmd-1.0#None': u'None', - u'http://musicbrainz.org/ns/mmd-1.0#Album': u'Album', - u'http://musicbrainz.org/ns/mmd-1.0#Single': u'Single', - u'http://musicbrainz.org/ns/mmd-1.0#EP': u'EP', - u'http://musicbrainz.org/ns/mmd-1.0#Compilation': u'Compilation', - u'http://musicbrainz.org/ns/mmd-1.0#Soundtrack': u'Soundtrack', - u'http://musicbrainz.org/ns/mmd-1.0#Spokenword': u'Spokenword', - u'http://musicbrainz.org/ns/mmd-1.0#Interview': u'Interview', - u'http://musicbrainz.org/ns/mmd-1.0#Audiobook': u'Audiobook', - u'http://musicbrainz.org/ns/mmd-1.0#Live': u'Live', - u'http://musicbrainz.org/ns/mmd-1.0#Remix': u'Remix', - u'http://musicbrainz.org/ns/mmd-1.0#Other': u'Other', - u'http://musicbrainz.org/ns/mmd-1.0#Official': u'Official', - u'http://musicbrainz.org/ns/mmd-1.0#Promotion': u'Promotion', - u'http://musicbrainz.org/ns/mmd-1.0#Bootleg': u'Bootleg', - u'http://musicbrainz.org/ns/mmd-1.0#Pseudo-Release': u'Pseudo-Release', -} - -# EOF diff --git a/lib/musicbrainz2/data/scriptnames.py b/lib/musicbrainz2/data/scriptnames.py deleted file mode 100644 index 30a55bd7..00000000 --- a/lib/musicbrainz2/data/scriptnames.py +++ /dev/null @@ -1,59 +0,0 @@ -# -*- coding: utf-8 -*- - -__revision__ = '$Id: scriptnames.py 7386 2006-04-30 11:12:55Z matt $' - -scriptNames = { - u'Yiii': u'Yi', - u'Telu': u'Telugu', - u'Taml': u'Tamil', - u'Guru': u'Gurmukhi', - u'Hebr': u'Hebrew', - u'Geor': u'Georgian (Mkhedruli)', - u'Ugar': u'Ugaritic', - u'Cyrl': u'Cyrillic', - u'Hrkt': u'Kanji & Kana', - u'Armn': u'Armenian', - u'Runr': u'Runic', - u'Khmr': u'Khmer', - u'Latn': u'Latin', - u'Hani': u'Han (Hanzi, Kanji, Hanja)', - u'Ital': u'Old Italic (Etruscan, Oscan, etc.)', - u'Hano': u'Hanunoo (Hanunóo)', - u'Ethi': u'Ethiopic (Ge\'ez)', - u'Gujr': u'Gujarati', - u'Hang': u'Hangul', - u'Arab': u'Arabic', - u'Thaa': u'Thaana', - u'Buhd': u'Buhid', - u'Sinh': u'Sinhala', - u'Orya': u'Oriya', - u'Hans': u'Han (Simplified variant)', - u'Thai': u'Thai', - u'Cprt': u'Cypriot', - u'Linb': u'Linear B', - u'Hant': u'Han (Traditional variant)', - u'Osma': u'Osmanya', - u'Mong': u'Mongolian', - u'Deva': u'Devanagari (Nagari)', - u'Laoo': u'Lao', - u'Tagb': u'Tagbanwa', - u'Hira': u'Hiragana', - u'Bopo': u'Bopomofo', - u'Goth': u'Gothic', - u'Tale': u'Tai Le', - u'Mymr': u'Myanmar (Burmese)', - u'Tglg': u'Tagalog', - u'Grek': u'Greek', - u'Mlym': u'Malayalam', - u'Cher': u'Cherokee', - u'Tibt': u'Tibetan', - u'Kana': u'Katakana', - u'Syrc': u'Syriac', - u'Cans': u'Unified Canadian Aboriginal Syllabics', - u'Beng': u'Bengali', - u'Limb': u'Limbu', - u'Ogam': u'Ogham', - u'Knda': u'Kannada', -} - -# EOF diff --git a/lib/musicbrainz2/disc.py b/lib/musicbrainz2/disc.py deleted file mode 100644 index 871c6084..00000000 --- a/lib/musicbrainz2/disc.py +++ /dev/null @@ -1,221 +0,0 @@ -"""Utilities for working with Audio CDs. - -This module contains utilities for working with Audio CDs. - -The functions in this module need both a working ctypes package (already -included in python-2.5) and an installed libdiscid. If you don't have -libdiscid, it can't be loaded, or your platform isn't supported by either -ctypes or this module, a C{NotImplementedError} is raised when using the -L{readDisc()} function. - -@author: Matthias Friedrich -""" -__revision__ = '$Id: disc.py 11987 2009-08-22 11:57:51Z matt $' - -import sys -import urllib -import urlparse -import ctypes -import ctypes.util -from lib.musicbrainz2.model import Disc - -__all__ = [ 'DiscError', 'readDisc', 'getSubmissionUrl' ] - - -class DiscError(IOError): - """The Audio CD could not be read. - - This may be simply because no disc was in the drive, the device name - was wrong or the disc can't be read. Reading errors can occur in case - of a damaged disc or a copy protection mechanism, for example. - """ - pass - - -def _openLibrary(): - """Tries to open libdiscid. - - @return: a C{ctypes.CDLL} object, representing the opened library - - @raise NotImplementedError: if the library can't be opened - """ - # This only works for ctypes >= 0.9.9.3. Any libdiscid is found, - # no matter how it's called on this platform. - try: - if hasattr(ctypes.cdll, 'find'): - libDiscId = ctypes.cdll.find('discid') - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - raise NotImplementedError('Error opening library: ' + str(e)) - - # Try to find the library using ctypes.util - libName = ctypes.util.find_library('discid') - if libName != None: - try: - libDiscId = ctypes.cdll.LoadLibrary(libName) - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - raise NotImplementedError('Error opening library: ' + - str(e)) - - # For compatibility with ctypes < 0.9.9.3 try to figure out the library - # name without the help of ctypes. We use cdll.LoadLibrary() below, - # which isn't available for ctypes == 0.9.9.3. - # - if sys.platform == 'linux2': - libName = 'libdiscid.so.0' - elif sys.platform == 'darwin': - libName = 'libdiscid.0.dylib' - elif sys.platform == 'win32': - libName = 'discid.dll' - else: - # This should at least work for Un*x-style operating systems - libName = 'libdiscid.so.0' - - try: - libDiscId = ctypes.cdll.LoadLibrary(libName) - _setPrototypes(libDiscId) - return libDiscId - except OSError, e: - raise NotImplementedError('Error opening library: ' + str(e)) - - assert False # not reached - - -def _setPrototypes(libDiscId): - ct = ctypes - libDiscId.discid_new.argtypes = ( ) - libDiscId.discid_new.restype = ct.c_void_p - - libDiscId.discid_free.argtypes = (ct.c_void_p, ) - - libDiscId.discid_read.argtypes = (ct.c_void_p, ct.c_char_p) - - libDiscId.discid_get_error_msg.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_error_msg.restype = ct.c_char_p - - libDiscId.discid_get_id.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_id.restype = ct.c_char_p - - libDiscId.discid_get_first_track_num.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_first_track_num.restype = ct.c_int - - libDiscId.discid_get_last_track_num.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_last_track_num.restype = ct.c_int - - libDiscId.discid_get_sectors.argtypes = (ct.c_void_p, ) - libDiscId.discid_get_sectors.restype = ct.c_int - - libDiscId.discid_get_track_offset.argtypes = (ct.c_void_p, ct.c_int) - libDiscId.discid_get_track_offset.restype = ct.c_int - - libDiscId.discid_get_track_length.argtypes = (ct.c_void_p, ct.c_int) - libDiscId.discid_get_track_length.restype = ct.c_int - - -def getSubmissionUrl(disc, host='mm.musicbrainz.org', port=80): - """Returns a URL for adding a disc to the MusicBrainz database. - - A fully initialized L{musicbrainz2.model.Disc} object is needed, as - returned by L{readDisc}. A disc object returned by the web service - doesn't provide the necessary information. - - Note that the created URL is intended for interactive use and points - to the MusicBrainz disc submission wizard by default. This method - just returns a URL, no network connection is needed. The disc drive - isn't used. - - @param disc: a fully initialized L{musicbrainz2.model.Disc} object - @param host: a string containing a host name - @param port: an integer containing a port number - - @return: a string containing the submission URL - - @see: L{readDisc} - """ - assert isinstance(disc, Disc), 'musicbrainz2.model.Disc expected' - discid = disc.getId() - first = disc.getFirstTrackNum() - last = disc.getLastTrackNum() - sectors = disc.getSectors() - assert None not in (discid, first, last, sectors) - - tracks = last - first + 1 - toc = "%d %d %d " % (first, last, sectors) - toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) ) - - query = urllib.urlencode({ 'id': discid, 'toc': toc, 'tracks': tracks }) - - if port == 80: - netloc = host - else: - netloc = host + ':' + str(port) - - url = ('http', netloc, '/bare/cdlookup.html', '', query, '') - - return urlparse.urlunparse(url) - - -def readDisc(deviceName=None): - """Reads an Audio CD in the disc drive. - - This reads a CD's table of contents (TOC) and calculates the MusicBrainz - DiscID, which is a 28 character ASCII string. This DiscID can be used - to retrieve a list of matching releases from the web service (see - L{musicbrainz2.webservice.Query}). - - Note that an Audio CD has to be in drive for this to work. The - C{deviceName} argument may be used to set the device. The default - depends on the operating system (on linux, it's C{'/dev/cdrom'}). - No network connection is needed for this function. - - If the device doesn't exist or there's no valid Audio CD in the drive, - a L{DiscError} exception is raised. - - @param deviceName: a string containing the CD drive's device name - - @return: a L{musicbrainz2.model.Disc} object - - @raise DiscError: if there was a problem reading the disc - @raise NotImplementedError: if DiscID generation isn't supported - """ - libDiscId = _openLibrary() - - handle = libDiscId.discid_new() - assert handle != 0, "libdiscid: discid_new() returned NULL" - - # Access the CD drive. This also works if deviceName is None because - # ctypes passes a NULL pointer in this case. - # - res = libDiscId.discid_read(handle, deviceName) - if res == 0: - raise DiscError(libDiscId.discid_get_error_msg(handle)) - - - # Now extract the data from the result. - # - disc = Disc() - - disc.setId( libDiscId.discid_get_id(handle) ) - - firstTrackNum = libDiscId.discid_get_first_track_num(handle) - lastTrackNum = libDiscId.discid_get_last_track_num(handle) - - disc.setSectors(libDiscId.discid_get_sectors(handle)) - - for i in range(firstTrackNum, lastTrackNum+1): - trackOffset = libDiscId.discid_get_track_offset(handle, i) - trackSectors = libDiscId.discid_get_track_length(handle, i) - - disc.addTrack( (trackOffset, trackSectors) ) - - disc.setFirstTrackNum(firstTrackNum) - disc.setLastTrackNum(lastTrackNum) - - libDiscId.discid_free(handle) - - return disc - -# EOF diff --git a/lib/musicbrainz2/model.py b/lib/musicbrainz2/model.py deleted file mode 100644 index fe8f05df..00000000 --- a/lib/musicbrainz2/model.py +++ /dev/null @@ -1,2488 +0,0 @@ -"""The MusicBrainz domain model. - -These classes are part of the MusicBrainz domain model. They may be used -by other modules and don't contain any network or other I/O code. If you -want to request data from the web service, please have a look at -L{musicbrainz2.webservice}. - -The most important classes, usually acting as entry points, are -L{Artist}, L{Release}, and L{Track}. - -@var VARIOUS_ARTISTS_ID: The ID of the special 'Various Artists' artist. - -@var NS_MMD_1: Default namespace prefix for all MusicBrainz metadata. -@var NS_REL_1: Namespace prefix for relations. -@var NS_EXT_1: Namespace prefix for MusicBrainz extensions. - -@see: L{musicbrainz2.webservice} - -@author: Matthias Friedrich -""" -try: - set -except NameError: - from sets import Set as set - -__revision__ = '$Id: model.py 12829 2010-09-15 12:00:11Z luks $' - -__all__ = [ - 'VARIOUS_ARTISTS_ID', 'NS_MMD_1', 'NS_REL_1', 'NS_EXT_1', - 'Entity', 'Artist', 'Release', 'Track', 'User', 'ReleaseGroup', - 'Relation', 'Disc', 'ReleaseEvent', 'Label', 'Tag', 'Rating', - 'AbstractAlias', 'ArtistAlias', 'LabelAlias', -] - - -VARIOUS_ARTISTS_ID = 'http://musicbrainz.org/artist/89ad4ac3-39f7-470e-963a-56509c546377' - -# Namespace URI prefixes -# -NS_MMD_1 = 'http://musicbrainz.org/ns/mmd-1.0#' -NS_REL_1 = 'http://musicbrainz.org/ns/rel-1.0#' -NS_EXT_1 = 'http://musicbrainz.org/ns/ext-1.0#' - - -class Entity(object): - """A first-level MusicBrainz class. - - All entities in MusicBrainz have unique IDs (which are absolute URIs) - as well as any number of L{relations } to other entities - and free text tags. This class is abstract and should not be - instantiated. - - Relations are differentiated by their I{target type}, that means, - where they link to. MusicBrainz currently supports four target types - (artists, releases, tracks, and URLs) each identified using a URI. - To get all relations with a specific target type, you can use - L{getRelations} and pass one of the following constants as the - parameter: - - - L{Relation.TO_ARTIST} - - L{Relation.TO_RELEASE} - - L{Relation.TO_TRACK} - - L{Relation.TO_URL} - - @see: L{Relation} - """ - - def __init__(self, id_=None): - """Constructor. - - This should only used by derived classes. - - @param id_: a string containing an absolute URI - """ - self._id = id_ - self._relations = { } - self._tags = { } - self._rating = Rating() - - def getId(self): - """Returns a MusicBrainz ID. - - @return: a string containing a URI, or None - """ - return self._id - - def setId(self, value): - """Sets a MusicBrainz ID. - - @param value: a string containing an absolute URI - """ - self._id = value - - id = property(getId, setId, doc='The MusicBrainz ID.') - - def getRelations(self, targetType=None, relationType=None, - requiredAttributes=(), direction=None): - """Returns a list of relations. - - If C{targetType} is given, only relations of that target - type are returned. For MusicBrainz, the following target - types are defined: - - L{Relation.TO_ARTIST} - - L{Relation.TO_RELEASE} - - L{Relation.TO_TRACK} - - L{Relation.TO_URL} - - If C{targetType} is L{Relation.TO_ARTIST}, for example, - this method returns all relations between this Entity and - artists. - - You may use the C{relationType} parameter to further restrict - the selection. If it is set, only relations with the given - relation type are returned. The C{requiredAttributes} sequence - lists attributes that have to be part of all returned relations. - - If C{direction} is set, only relations with the given reading - direction are returned. You can use the L{Relation.DIR_FORWARD}, - L{Relation.DIR_BACKWARD}, and L{Relation.DIR_NONE} constants - for this. - - @param targetType: a string containing an absolute URI, or None - @param relationType: a string containing an absolute URI, or None - @param requiredAttributes: a sequence containing absolute URIs - @param direction: one of L{Relation}'s direction constants - @return: a list of L{Relation} objects - - @see: L{Entity} - """ - allRels = [ ] - if targetType is not None: - allRels = self._relations.setdefault(targetType, [ ]) - else: - for (k, relList) in self._relations.items(): - for rel in relList: - allRels.append(rel) - - # Filter for direction. - # - if direction is not None: - allRels = [r for r in allRels if r.getDirection() == direction] - - # Filter for relation type. - # - if relationType is None: - return allRels - else: - allRels = [r for r in allRels if r.getType() == relationType] - - # Now filer for attribute type. - # - tmp = [] - required = set(iter(requiredAttributes)) - - for r in allRels: - attrs = set(iter(r.getAttributes())) - if required.issubset(attrs): - tmp.append(r) - return tmp - - - def getRelationTargets(self, targetType=None, relationType=None, - requiredAttributes=(), direction=None): - """Returns a list of relation targets. - - The arguments work exactly like in L{getRelations}, but - instead of L{Relation} objects, the matching relation - targets are returned. This can be L{Artist}, L{Release}, - or L{Track} objects, depending on the relations. - - As a special case, URL strings are returned if the target - is an URL. - - @param targetType: a string containing an absolute URI, or None - @param relationType: a string containing an absolute URI, or None - @param requiredAttributes: a sequence containing absolute URIs - @param direction: one of L{Relation}'s direction constants - @return: a list of objects, depending on the relation - - @see: L{getRelations} - """ - ret = [ ] - rels = self.getRelations(targetType, relationType, - requiredAttributes, direction) - - for r in rels: - if r.getTargetType() == Relation.TO_URL: - ret.append(r.getTargetId()) - else: - ret.append(r.getTarget()) - - return ret - - - def addRelation(self, relation): - """Adds a relation. - - This method adds C{relation} to the list of relations. The - given relation has to be initialized, at least the target - type has to be set. - - @param relation: the L{Relation} object to add - - @see: L{Entity} - """ - assert relation.getType is not None - assert relation.getTargetType is not None - assert relation.getTargetId is not None - l = self._relations.setdefault(relation.getTargetType(), [ ]) - l.append(relation) - - - def getRelationTargetTypes(self): - """Returns a list of target types available for this entity. - - Use this to find out to which types of targets this entity - has relations. If the entity only has relations to tracks and - artists, for example, then a list containg the strings - L{Relation.TO_TRACK} and L{Relation.TO_ARTIST} is returned. - - @return: a list of strings containing URIs - - @see: L{getRelations} - """ - return self._relations.keys() - - def getTag(self, value): - """Return the tag with the given value (aka the tag's name). - - @return: the L{Tag} with the given name or raises a KeyError - """ - return self._tags[value] - - def getTags(self): - """Return all tags attached to this Entity. - - @return: a list of L{Tag} objects - """ - return self._tags.values() - - tags = property(getTags, doc='The tags for this entity.') - - def addTag(self, tag): - """Add a new tag. - - This merges an existing tag with the same name. - - @param tag: the L{Tag} object to add - - @see: L{getTags} - """ - if self._tags.has_key(tag.value): - existing = self._tags[tag.value] - existing.count += tag.count - else: - self._tags[tag.value] = tag - - def getRating(self): - """Return the rating of this Entity. - 0 = Unrated - 1 - 5 = Rating - - @return: rating - """ - return self._rating - - rating = property(getRating, doc='The rating for this entity.') - - def setRating(self, value): - self._rating = value - - -class Artist(Entity): - """Represents an artist. - - Artists in MusicBrainz can have a type. Currently, this type can - be either Person or Group for which the following URIs are assigned: - - - C{http://musicbrainz.org/ns/mmd-1.0#Person} - - C{http://musicbrainz.org/ns/mmd-1.0#Group} - - Use the L{TYPE_PERSON} and L{TYPE_GROUP} constants for comparison. - """ - TYPE_PERSON = NS_MMD_1 + 'Person' - TYPE_GROUP = NS_MMD_1 + 'Group' - - def __init__(self, id_=None, type_=None, name=None, sortName=None): - """Constructor. - - @param id_: a string containing an absolute URI - @param type_: a string containing an absolute URI - @param name: a string containing the artist's name - @param sortName: a string containing the artist's sort name - """ - Entity.__init__(self, id_) - self._type = type_ - self._name = name - self._sortName = sortName - self._disambiguation = None - self._beginDate = None - self._endDate = None - self._aliases = [ ] - self._releases = [ ] - self._releasesCount = None - self._releasesOffset = None - self._releaseGroups = [ ] - self._releaseGroupsCount = None - self._releaseGroupsOffset = None - - def getType(self): - """Returns the artist's type. - - @return: a string containing an absolute URI, or None - """ - return self._type - - def setType(self, type_): - """Sets the artist's type. - - @param type_: a string containing an absolute URI - """ - self._type = type_ - - type = property(getType, setType, doc="The artist's type.") - - def getName(self): - """Returns the artist's name. - - @return: a string containing the artist's name, or None - """ - return self._name - - def setName(self, name): - """Sets the artist's name. - - @param name: a string containing the artist's name - """ - self._name = name - - name = property(getName, setName, doc="The artist's name.") - - def getSortName(self): - """Returns the artist's sort name. - - The sort name is the artist's name in a special format which - is better suited for lexicographic sorting. The MusicBrainz - style guide specifies this format. - - @see: U{The MusicBrainz Style Guidelines - } - """ - return self._sortName - - def setSortName(self, sortName): - """Sets the artist's sort name. - - @param sortName: a string containing the artist's sort name - - @see: L{getSortName} - """ - self._sortName = sortName - - sortName = property(getSortName, setSortName, - doc="The artist's sort name.") - - def getDisambiguation(self): - """Returns the disambiguation attribute. - - This attribute may be used if there is more than one artist - with the same name. In this case, disambiguation attributes - are added to the artists' names to keep them apart. - - For example, there are at least three bands named 'Vixen'. - Each band has a different disambiguation in the MusicBrainz - database, like 'Hip-hop' or 'all-female rock/glam band'. - - @return: a disambiguation string, or None - - @see: L{getUniqueName} - """ - return self._disambiguation - - def setDisambiguation(self, disambiguation): - """Sets the disambiguation attribute. - - @param disambiguation: a disambiguation string - - @see: L{getDisambiguation}, L{getUniqueName} - """ - self._disambiguation = disambiguation - - disambiguation = property(getDisambiguation, setDisambiguation, - doc="The disambiguation comment.") - - def getUniqueName(self): - """Returns a unique artist name (using disambiguation). - - This method returns the artist name together with the - disambiguation attribute in parenthesis if it exists. - Example: 'Vixen (Hip-hop)'. - - @return: a string containing the unique name - - @see: L{getDisambiguation} - """ - d = self.getDisambiguation() - if d is not None and d.strip() != '': - return '%s (%s)' % (self.getName(), d) - else: - return self.getName() - - def getBeginDate(self): - """Returns the birth/foundation date. - - The definition of the I{begin date} depends on the artist's - type. For persons, this is the day of birth, for groups it - is the day the group was founded. - - The returned date has the format 'YYYY', 'YYYY-MM', or - 'YYYY-MM-DD', depending on how much detail is known. - - @return: a string containing the date, or None - - @see: L{getType} - """ - return self._beginDate - - def setBeginDate(self, dateStr): - """Sets the begin/foundation date. - - @param dateStr: a date string - - @see: L{getBeginDate} - """ - self._beginDate = dateStr - - beginDate = property(getBeginDate, setBeginDate, - doc="The begin/foundation date.") - - def getEndDate(self): - """Returns the death/dissolving date. - - The definition of the I{end date} depends on the artist's - type. For persons, this is the day of death, for groups it - is the day the group was dissolved. - - @return: a string containing a date, or None - - @see: L{getBeginDate} - """ - return self._endDate - - def setEndDate(self, dateStr): - """Sets the death/dissolving date. - - @param dateStr: a string containing a date - - @see: L{setEndDate}, L{getBeginDate} - """ - self._endDate = dateStr - - endDate = property(getEndDate, setEndDate, - doc="The death/dissolving date.") - - def getAliases(self): - """Returns the list of aliases for this artist. - - @return: a list of L{ArtistAlias} objects - """ - return self._aliases - - aliases = property(getAliases, doc='The list of aliases.') - - def addAlias(self, alias): - """Adds an alias for this artist. - - @param alias: an L{ArtistAlias} object - """ - self._aliases.append(alias) - - def getReleases(self): - """Returns a list of releases from this artist. - - This may also include releases where this artist isn't the - I{main} artist but has just contributed one or more tracks - (aka VA-Releases). - - @return: a list of L{Release} objects - """ - return self._releases - - releases = property(getReleases, doc='The list of releases') - - def addRelease(self, release): - """Adds a release to this artist's list of releases. - - @param release: a L{Release} object - """ - self._releases.append(release) - - def getReleasesOffset(self): - """Returns the offset of the release list. - - This is used if the release list is incomplete (ie. the web - service only returned part of the release for this artist). - Note that the offset value is zero-based, which means release - C{0} is the first release. - - @return: an integer containing the offset, or None - - @see: L{getReleases}, L{getReleasesCount} - """ - return self._releasesOffset - - def setReleasesOffset(self, offset): - """Sets the offset of the release list. - - @param offset: an integer containing the offset, or None - - @see: L{getReleasesOffset} - """ - self._releasesOffset = offset - - releasesOffset = property(getReleasesOffset, setReleasesOffset, - doc='The offset of the release list.') - - def getReleasesCount(self): - """Returns the number of existing releases. - - This may or may not match with the number of elements that - L{getReleases} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setReleasesCount}, L{getReleasesOffset} - """ - return self._releasesCount - - def setReleasesCount(self, value): - """Sets the number of existing releases. - - @param value: an integer containing the count, or None - - @see: L{getReleasesCount}, L{setReleasesOffset} - """ - self._releasesCount = value - - releasesCount = property(getReleasesCount, setReleasesCount, - doc='The total number of releases') - - def getReleaseGroups(self): - """Returns a list of release groups from this artist. - - @return: a list of L{ReleaseGroup} objects - """ - return self._releaseGroups - - releaseGroups = property(getReleaseGroups, doc='The list of release groups') - - def addReleaseGroup(self, releaseGroup): - """Adds a release group to this artist's list of release groups. - - @param releaseGroup: a L{ReleaseGroup} object - """ - self._releaseGroups.append(releaseGroup) - - def getReleaseGroupsOffset(self): - """Returns the offset of the release group list. - - This is used if the release group list is incomplete (ie. the - web service only returned part of the result for this artist). - Note that the offset value is zero-based, which means release - group C{0} is the first release group. - - @return: an integer containing the offset, or None - - @see: L{getReleaseGroups}, L{getReleaseGroupsCount} - """ - return self._releaseGroupsOffset - - def setReleaseGroupsOffset(self, offset): - """Sets the offset of the release group list. - - @param offset: an integer containing the offset, or None - - @see: L{getReleaseGroupsOffset} - """ - self._releaseGroupsOffset = offset - - releaseGroupsOffset = property(getReleaseGroupsOffset, setReleaseGroupsOffset, - doc='The offset of the release group list.') - - def getReleaseGroupsCount(self): - """Returns the number of existing release groups. - - This may or may not match with the number of elements that - L{getReleaseGroups} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setReleaseGroupsCount}, L{getReleaseGroupsOffset} - """ - return self._releaseGroupsCount - - def setReleaseGroupsCount(self, value): - """Sets the number of existing release groups. - - @param value: an integer containing the count, or None - - @see: L{getReleaseGroupsCount}, L{setReleaseGroupsOffset} - """ - self._releaseGroupsCount = value - - releasesCount = property(getReleaseGroupsCount, setReleaseGroupsCount, - doc='The total number of release groups') - - -class Rating(object): - """The representation of a MusicBrain rating. - - The rating can have the following values: - - 0 = Unrated - [1..5] = Rating - """ - def __init__(self, value=None, count=None): - """Constructor. - - @param value: a string containing the tag's value - @param count: the number of users who added this tag - """ - self._value = value - self._count = count - - def getValue(self): - """Returns a string with the tag's value. - - @return: an integer containing the rating's value, or None - """ - return self._value - - def setValue(self, value): - """ Set the value of this rating. - - 0 or None = Clear your rating - 1 - 5 = Rating - - @param value: the rating to apply - - @raise ValueError: if value is not a double or not in the - range 0 - 5 or None. - """ - if value == None: - value = 0 - try: - value = float(value) - except ValueError, e: - raise ValueError("Value for rating needs to be an" \ - "float.") - if value < 0.0 or value > 5.0: - raise ValueError("Value needs to be in the range [0..5]") - self._value = value - - value = property(getValue, setValue, doc='The value of the rating.') - - def getCount(self): - """Returns an integer containing the rating's frequency count. - - @return: an integer containing the rating's frequency count, - or None - """ - return self._count - - def setCount(self, count): - """Sets the frequency count of this rating. - - @param count: an integer containing the tag's frequency count - """ - self._count = count - - count = property(getCount, setCount, doc="This tag's frequency count.") - - def __str__(self): - return str(self._value) - - def __unicode__(self): - return unicode(self._value) - - -class Tag(object): - """The representation of a MusicBrainz folksonomy tag. - - The tag's value is the text that's displayed in the tag cloud. - The count attribute keeps track of how many users added the tag - to its owning entity. - """ - def __init__(self, value=None, count=None): - """Constructor. - - @param value: a string containing the tag's value - @param count: the number of users who added this tag - """ - self._value = value - self._count = count - - def getValue(self): - """Returns a string with the tag's value. - - @return: a string containing the tags's value, or None - """ - return self._value - - def setValue(self, value): - """Sets the value of this tag. - - @param value: A string containing the value of the tag - """ - self._value = value - - value = property(getValue, setValue, doc='The value of the text.') - - def getCount(self): - """Returns an integer containing the tag's frequency count. - - @return: an integer containing the tags's frequency count, or None - """ - return self._count - - def setCount(self, count): - """Sets the frequency count of this tag. - - @param count: an integer containing the tag's frequency count - """ - self._count = count - - count = property(getCount, setCount, doc="This tag's frequency count.") - - def __str__(self): - return str(self._value) - - def __unicode__(self): - return unicode(self._value) - - -class Label(Entity): - """Represents a record label. - - A label within MusicBrainz is an L{Entity}. It contains information - about the label like when it was established, its name, label code and - other relationships. All release events may be assigned a label. - """ - TYPE_UNKNOWN = NS_MMD_1 + 'Unknown' - - TYPE_DISTRIBUTOR = NS_MMD_1 + 'Distributor' - TYPE_HOLDING = NS_MMD_1 + 'Holding' - TYPE_PRODUCTION = NS_MMD_1 + 'Production' - - TYPE_ORIGINAL = NS_MMD_1 + 'OriginalProduction' - TYPE_BOOTLEG = NS_MMD_1 + 'BootlegProduction' - TYPE_REISSUE = NS_MMD_1 + 'ReissueProduction' - - def __init__(self, id_=None): - """Constructor. - - @param id_: a string containing an absolute URI - """ - Entity.__init__(self, id_) - self._type = None - self._name = None - self._sortName = None - self._disambiguation = None - self._countryId = None - self._code = None - self._beginDate = None - self._endDate = None - self._aliases = [ ] - - def getType(self): - """Returns the type of this label. - - @return: a string containing an absolute URI - """ - return self._type - - def setType(self, type_): - """Sets the type of this label. - - @param type_: A string containing the absolute URI of the type of label. - """ - self._type = type_ - - type = property(getType, setType, doc='The type of label') - - def getName(self): - """Returns a string with the name of the label. - - @return: a string containing the label's name, or None - """ - return self._name - - def setName(self, name): - """Sets the name of this label. - - @param name: A string containing the name of the label - """ - self._name = name - - name = property(getName, setName, doc='The name of the label.') - - def getSortName(self): - """Returns the label's sort name. - - The sort name is the label's name in a special format which - is better suited for lexicographic sorting. The MusicBrainz - style guide specifies this format. - - @see: U{The MusicBrainz Style Guidelines - } - """ - return self._sortName - - def setSortName(self, sortName): - """Sets the label's sort name. - - @param sortName: a string containing the label's sort name - - @see: L{getSortName} - """ - self._sortName = sortName - - sortName = property(getSortName, setSortName, - doc="The label's sort name.") - - def getDisambiguation(self): - """Returns the disambiguation attribute. - - This attribute may be used if there is more than one label - with the same name. In this case, disambiguation attributes - are added to the labels' names to keep them apart. - - @return: a disambiguation string, or None - - @see: L{getUniqueName} - """ - return self._disambiguation - - def setDisambiguation(self, disambiguation): - """Sets the disambiguation attribute. - - @param disambiguation: a disambiguation string - - @see: L{getDisambiguation}, L{getUniqueName} - """ - self._disambiguation = disambiguation - - disambiguation = property(getDisambiguation, setDisambiguation, - doc="The disambiguation comment.") - - def getUniqueName(self): - """Returns a unique label name (using disambiguation). - - This method returns the label's name together with the - disambiguation attribute in parenthesis if it exists. - - @return: a string containing the unique name - - @see: L{getDisambiguation} - """ - d = self.getDisambiguation() - if d is not None and d.strip() != '': - return '%s (%s)' % (self.getName(), d) - else: - return self.getName() - - def getBeginDate(self): - """Returns the date this label was established. - - @return: A string contained the start date, or None - """ - return self._beginDate - - def setBeginDate(self, date): - """Set the date this label was established. - - @param date: A string in the format of YYYY-MM-DD - """ - self._beginDate = date - - beginDate = property(getBeginDate, setBeginDate, - doc='The date this label was established.') - - def getEndDate(self): - """Returns the date this label closed. - - The returned date has the format 'YYYY', 'YYYY-MM', or - 'YYYY-MM-DD', depending on how much detail is known. - - @return: A string containing the date, or None - """ - return self._endDate - - def setEndDate(self, date): - """Set the date this label closed. - - The date may have the format 'YYYY', 'YYYY-MM', or - 'YYYY-MM-DD', depending on how much detail is known. - - @param date: A string containing the date, or None - """ - self._endDate = date - - endDate = property(getEndDate, setEndDate, - doc='The date this label closed.') - - def getCountry(self): - """Returns the country the label is located. - - @return: a string containing an ISO-3166 country code, or None - - @see: L{musicbrainz2.utils.getCountryName} - """ - return self._countryId - - def setCountry(self, country): - """Sets the country the label is located. - - @param country: a string containing an ISO-3166 country code - """ - self._countryId = country - - country = property(getCountry, setCountry, - doc='The country the label is located.') - - def getCode(self): - """Returns the label code. - - Label codes have been introduced by the IFPI (International - Federation of Phonogram and Videogram Industries) to uniquely - identify record labels. The label code consists of 'LC-' and 4 - figures (currently being extended to 5 figures). - - @return: a string containing the label code, or None - """ - return self._code - - def setCode(self, code): - """Sets the label code. - - @param code: a string containing the label code - """ - self._code = code - - code = property(getCode, setCode, - doc='The label code.') - - def getAliases(self): - """Returns the list of aliases for this label. - - @return: a list of L{LabelAlias} objects - """ - return self._aliases - - aliases = property(getAliases, doc='The list of aliases.') - - def addAlias(self, alias): - """Adds an alias for this label. - - @param alias: a L{LabelAlias} object - """ - self._aliases.append(alias) - - -class Release(Entity): - """Represents a Release. - - A release within MusicBrainz is an L{Entity} which contains L{Track} - objects. Releases may be of more than one type: There can be albums, - singles, compilations, live recordings, official releases, bootlegs - etc. - - @note: The current MusicBrainz server implementation supports only a - limited set of types. - """ - TYPE_NONE = NS_MMD_1 + 'None' - TYPE_NON_ALBUM_TRACKS = NS_MMD_1 + "NonAlbum Track" - - TYPE_ALBUM = NS_MMD_1 + 'Album' - TYPE_SINGLE = NS_MMD_1 + 'Single' - TYPE_EP = NS_MMD_1 + 'EP' - TYPE_COMPILATION = NS_MMD_1 + 'Compilation' - TYPE_SOUNDTRACK = NS_MMD_1 + 'Soundtrack' - TYPE_SPOKENWORD = NS_MMD_1 + 'Spokenword' - TYPE_INTERVIEW = NS_MMD_1 + 'Interview' - TYPE_AUDIOBOOK = NS_MMD_1 + 'Audiobook' - TYPE_LIVE = NS_MMD_1 + 'Live' - TYPE_REMIX = NS_MMD_1 + 'Remix' - TYPE_OTHER = NS_MMD_1 + 'Other' - - TYPE_OFFICIAL = NS_MMD_1 + 'Official' - TYPE_PROMOTION = NS_MMD_1 + 'Promotion' - TYPE_BOOTLEG = NS_MMD_1 + 'Bootleg' - TYPE_PSEUDO_RELEASE = NS_MMD_1 + 'Pseudo-Release' - - def __init__(self, id_=None, title=None): - """Constructor. - - @param id_: a string containing an absolute URI - @param title: a string containing the title - """ - Entity.__init__(self, id_) - self._types = [ ] - self._title = title - self._textLanguage = None - self._textScript = None - self._asin = None - self._artist = None - self._releaseEvents = [ ] - #self._releaseEventsCount = None - self._releaseGroup = None - self._discs = [ ] - #self._discIdsCount = None - self._tracks = [ ] - self._tracksOffset = None - self._tracksCount = None - - - def getTypes(self): - """Returns the types of this release. - - To test for release types, you can use the constants - L{TYPE_ALBUM}, L{TYPE_SINGLE}, etc. - - @return: a list of strings containing absolute URIs - - @see: L{musicbrainz2.utils.getReleaseTypeName} - """ - return self._types - - types = property(getTypes, doc='The list of types for this release.') - - def addType(self, type_): - """Add a type to the list of types. - - @param type_: a string containing absolute URIs - - @see: L{getTypes} - """ - self._types.append(type_) - - def getTitle(self): - """Returns the release's title. - - @return: a string containing the release's title - """ - return self._title - - def setTitle(self, title): - """Sets the release's title. - - @param title: a string containing the release's title, or None - """ - self._title = title - - title = property(getTitle, setTitle, doc='The title of this release.') - - def getTextLanguage(self): - """Returns the language used in release and track titles. - - To represent the language, the ISO-639-2/T standard is used, - which provides three-letter terminological language codes like - 'ENG', 'DEU', 'JPN', 'KOR', 'ZHO' or 'YID'. - - Note that this refers to release and track I{titles}, not - lyrics. - - @return: a string containing the language code, or None - - @see: L{musicbrainz2.utils.getLanguageName} - """ - return self._textLanguage - - def setTextLanguage(self, language): - """Sets the language used in releaes and track titles. - - @param language: a string containing a language code - - @see: L{getTextLanguage} - """ - self._textLanguage = language - - textLanguage = property(getTextLanguage, setTextLanguage, - doc='The language used in release and track titles.') - - def getTextScript(self): - """Returns the script used in release and track titles. - - To represent the script, ISO-15924 script codes are used. - Valid codes are, among others: 'Latn', 'Cyrl', 'Hans', 'Hebr' - - Note that this refers to release and track I{titles}, not - lyrics. - - @return: a string containing the script code, or None - - @see: L{musicbrainz2.utils.getScriptName} - """ - return self._textScript - - def setTextScript(self, script): - """Sets the script used in releaes and track titles. - - @param script: a string containing a script code - - @see: L{getTextScript} - """ - self._textScript = script - - textScript = property(getTextScript, setTextScript, - doc='The script used in release and track titles.') - - def getAsin(self): - """Returns the amazon shop identifier (ASIN). - - The ASIN is a 10-letter code (except for books) assigned - by Amazon, which looks like 'B000002IT2' or 'B00006I4YD'. - - @return: a string containing the ASIN, or None - """ - return self._asin - - def setAsin(self, asin): - """Sets the amazon shop identifier (ASIN). - - @param asin: a string containing the ASIN - - @see: L{getAsin} - """ - self._asin = asin - - asin = property(getAsin, setAsin, doc='The amazon shop identifier.') - - def getArtist(self): - """Returns the main artist of this release. - - @return: an L{Artist} object, or None - """ - return self._artist - - def setArtist(self, artist): - """Sets this release's main artist. - - @param artist: an L{Artist} object - """ - self._artist = artist - - artist = property(getArtist, setArtist, - doc='The main artist of this release.') - - def getReleaseGroup(self): - """Returns the release group to which this release belongs. - - @return: a L{ReleaseGroup} object, or None. - """ - return self._releaseGroup - - def setReleaseGroup(self, releaseGroup): - """Sets the release's release group. - - @param releaseGroup: a L{ReleaseGroup} object, or None. - """ - self._releaseGroup = releaseGroup - - releaseGroup = property(getReleaseGroup, setReleaseGroup, - doc='The release group this release belongs to.') - - def isSingleArtistRelease(self): - """Checks if this is a single artist's release. - - Returns C{True} if the release's main artist (L{getArtist}) is - also the main artist for all of the tracks. This is checked by - comparing the artist IDs. - - Note that the release's artist has to be set (see L{setArtist}) - for this. The track artists may be unset. - - @return: True, if this is a single artist's release - """ - releaseArtist = self.getArtist() - assert releaseArtist is not None, 'Release Artist may not be None!' - for track in self.getTracks(): - if track.getArtist() is None: - continue - if track.getArtist().getId() != releaseArtist.getId(): - return False - - return True - - def getTracks(self): - """Returns the tracks this release contains. - - @return: a list containing L{Track} objects - - @see: L{getTracksOffset}, L{getTracksCount} - """ - return self._tracks - - tracks = property(getTracks, doc='The list of tracks.') - - def addTrack(self, track): - """Adds a track to this release. - - This appends a track at the end of this release's track list. - - @param track: a L{Track} object - """ - self._tracks.append(track) - - def getTracksOffset(self): - """Returns the offset of the track list. - - This is used if the track list is incomplete (ie. the web - service only returned part of the tracks on this release). - Note that the offset value is zero-based, which means track - C{0} is the first track. - - @return: an integer containing the offset, or None - - @see: L{getTracks}, L{getTracksCount} - """ - return self._tracksOffset - - def setTracksOffset(self, offset): - """Sets the offset of the track list. - - @param offset: an integer containing the offset, or None - - @see: L{getTracksOffset}, L{setTracksCount} - """ - self._tracksOffset = offset - - tracksOffset = property(getTracksOffset, setTracksOffset, - doc='The offset of the track list.') - - def getTracksCount(self): - """Returns the number of tracks on this release. - - This may or may not match with the number of elements that - L{getTracks} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setTracksCount}, L{getTracks}, L{getTracksOffset} - """ - return self._tracksCount - - def setTracksCount(self, value): - """Sets the number of tracks on this release. - - @param value: an integer containing the count, or None - - @see: L{getTracksCount}, L{setTracksOffset} - """ - self._tracksCount = value - - tracksCount = property(getTracksCount, setTracksCount, - doc='The total number of releases') - - - def getReleaseEvents(self): - """Returns the list of release events. - - A L{Release} may contain a list of so-called release events, - each represented using a L{ReleaseEvent} object. Release - evens specify where and when this release was, well, released. - - @return: a list of L{ReleaseEvent} objects - - @see: L{getReleaseEventsAsDict} - """ - return self._releaseEvents - - releaseEvents = property(getReleaseEvents, - doc='The list of release events.') - - def addReleaseEvent(self, event): - """Adds a release event to this release. - - @param event: a L{ReleaseEvent} object - - @see: L{getReleaseEvents} - """ - self._releaseEvents.append(event) - - def getReleaseEventsAsDict(self): - """Returns the release events represented as a dict. - - Keys are ISO-3166 country codes like 'DE', 'UK', 'FR' etc. - Values are dates in 'YYYY', 'YYYY-MM' or 'YYYY-MM-DD' format. - - @return: a dict containing (countryCode, date) entries - - @see: L{getReleaseEvents}, L{musicbrainz2.utils.getCountryName} - """ - d = { } - for event in self.getReleaseEvents(): - d[event.getCountry()] = event.getDate() - return d - - def getEarliestReleaseDate(self): - """Returns the earliest release date. - - This favours complete dates. For example, '2006-09' is - returned if there is '2000', too. If there is no release - event associated with this release, None is returned. - - @return: a string containing the date, or None - - @see: L{getReleaseEvents}, L{getReleaseEventsAsDict} - """ - event = self.getEarliestReleaseEvent() - - if event is None: - return None - else: - return event.getDate() - - def getEarliestReleaseEvent(self): - """Returns the earliest release event. - - This works like L{getEarliestReleaseDate}, but instead of - just the date, this returns a L{ReleaseEvent} object. - - @return: a L{ReleaseEvent} object, or None - - @see: L{getReleaseEvents}, L{getEarliestReleaseDate} - """ - dates = [ ] - for event in self.getReleaseEvents(): - date = event.getDate() - if len(date) == 10: # 'YYYY-MM-DD' - dates.append( (date, event) ) - elif len(date) == 7: # 'YYYY-MM' - dates.append( (date + '-99', event) ) - else: - dates.append( (date + '-99-99', event) ) - - dates.sort(lambda x, y: cmp(x[0], y[0])) - - if len(dates) > 0: - return dates[0][1] - else: - return None - - - #def getReleaseEventsCount(self): - # """Returns the number of release events. - # - # This may or may not match with the number of elements that - # getReleaseEvents() returns. If the count is higher than - # the list, it indicates that the list is incomplete. - # """ - # return self._releaseEventsCount - - #def setReleaseEventsCount(self, value): - # self._releaseEventsCount = value - - def getDiscs(self): - """Returns the discs associated with this release. - - Discs are currently containers for MusicBrainz DiscIDs. - Note that under rare circumstances (identical TOCs), a - DiscID could be associated with more than one release. - - @return: a list of L{Disc} objects - """ - return self._discs - - discs = property(getDiscs, doc='The list of associated discs.') - - def addDisc(self, disc): - """Adds a disc to this release. - - @param disc: a L{Disc} object - """ - self._discs.append(disc) - - #def getDiscIdsCount(self): - # return self._discIdsCount - - #def setDiscIdsCount(self, value): - # self._discIdsCount = value - - -class ReleaseGroup(Entity): - """Represents a ReleaseGroup. - - A ReleaseGroup in MusicBrainz is an L{Entity} which groups several different - versions of L{Release} objects (e.g., different editions of the same album). - - @see: L{Release} - @see: L{Entity} - """ - - def __init__(self, id_=None, title=None): - """Constructor. - - @param id_: a string containing an absolute URI - @param title: a string containing the title - """ - Entity.__init__(self, id_) - self._title = title - self._id = id_ - self._type = None - self._releases = [ ] - self._artist = None - self._releasesOffset = 0 - self._releasesCount = 0 - - def getType(self): - """Returns the type of this release group. - - To test for release types, you can use the constants - L{Release.TYPE_ALBUM}, L{Release.TYPE_SINGLE}, etc. - - @return: a string containing an absolute URI, or None. - - @see: L{musicbrainz2.utils.getReleaseTypeName} - """ - return self._type - - def setType(self, type_): - """Sets the type of this release group. - - Use a constant from the L{Release} class, such as - L{Release.TYPE_ALBUM} or L{Release.TYPE_SINGLE} to - set the value. - - @param type_: a string containing an absolute URI, or None. - - @see: L{musicbrainz2.utils.getReleaseTypeName} - """ - self._type = type_ - - type = property(getType, setType, - doc = 'The type of this release group.') - - def getReleases(self): - """Gets the releases in this release group. - - @return: a list of L{Release} objects - @see: L{Release} - """ - return self._releases - - releases = property(getReleases, - doc = 'The list of releases in this release group.') - - def addRelease(self, release): - """Adds a L{Release} to this release group. - - @param release: a L{Release} object - """ - self._releases.append(release) - - def getReleasesOffset(self): - """Returns the offset of the release list. - - This is used if the release list is incomplete (i.e., the web - service only returned a portion of the releases in this release - group). - - @return: an integer containing the offset, or None. - @see: L{getReleases}, L{getReleasesCount} - """ - return self._releasesOffset - - def setReleasesOffset(self, offset): - """Sets the offset of the release list. - - @param offset: an integer containing the offset, or None. - @see: L{getReleases}, L{getReleasesOffset} - """ - self._releasesOffset = offset - - releasesOffset = property(getReleasesOffset, setReleasesOffset, - doc='The offset of the release list.') - - def getReleasesCount(self): - """Returns the number of releases in this release group. - - This may or may not match the number of elements returned by - L{getReleases}. If the count is higher than the length of that - list, then the list is incomplete. - - @return: an integer containing the count, or None - @see: L{getReleases}, L{setReleasesCount}, L{getReleasesOffset} - """ - return self._releasesCount - - def setReleasesCount(self, value): - """Sets the number of releases in this release group. - - @param value: an integer containing the count, or None. - @see: L{getReleases}, L{getReleasesCount}, L{getReleasesOffset} - """ - self._releasesCount = value - - releasesCount = property(getReleasesCount, setReleasesCount, - doc = 'The total number of releases') - - def getTitle(self): - """Returns this release group's title. - - @return: a string containing the release group's title - """ - return self._title - - def setTitle(self, title): - """Sets the release group's title. - - @param title: a string containing the release group's title. - """ - self._title = title - - title = property(getTitle, setTitle, - doc = 'The title of this release group.') - - def getArtist(self): - """Returns the main artist of this release group. - - @return: an L{Artist} object, or None - """ - return self._artist - - def setArtist(self, artist): - """Sets the release group's main artist. - - @param artist: an L{Artist} object - """ - self._artist = artist - - artist = property(getArtist, setArtist, - doc = 'The main artist of this release group') - - -class Track(Entity): - """Represents a track. - - This class represents a track which may appear on one or more releases. - A track may be associated with exactly one artist (the I{main} artist). - - Using L{getReleases}, you can find out on which releases this track - appears. To get the track number, too, use the - L{Release.getTracksOffset} method. - - @note: Currently, the MusicBrainz server doesn't support tracks to - be on more than one release. - - @see: L{Release}, L{Artist} - """ - def __init__(self, id_=None, title=None): - """Constructor. - - @param id_: a string containing an absolute URI - @param title: a string containing the title - """ - Entity.__init__(self, id_) - self._title = title - self._artist = None - self._duration = None - self._puids = [ ] - self._releases = [ ] - self._isrcs = [ ] - - def getTitle(self): - """Returns the track's title. - - The style and format of this attribute is specified by the - style guide. - - @return: a string containing the title, or None - - @see: U{The MusicBrainz Style Guidelines - } - """ - return self._title - - def setTitle(self, title): - """Sets the track's title. - - @param title: a string containing the title - - @see: L{getTitle} - """ - self._title = title - - title = property(getTitle, setTitle, doc="The track's title.") - - def getArtist(self): - """Returns the main artist of this track. - - @return: an L{Artist} object, or None - """ - return self._artist - - def setArtist(self, artist): - """Sets this track's main artist. - - @param artist: an L{Artist} object - """ - self._artist = artist - - artist = property(getArtist, setArtist, doc="The track's main artist.") - - def getDuration(self): - """Returns the duration of this track in milliseconds. - - @return: an int containing the duration in milliseconds, or None - """ - return self._duration - - def setDuration(self, duration): - """Sets the duration of this track in milliseconds. - - @param duration: an int containing the duration in milliseconds - """ - self._duration = duration - - duration = property(getDuration, setDuration, - doc='The duration in milliseconds.') - - def getDurationSplit(self): - """Returns the duration as a (minutes, seconds) tuple. - - If no duration is set, (0, 0) is returned. Seconds are - rounded towards the ceiling if at least 500 milliseconds - are left. - - @return: a (minutes, seconds) tuple, both entries being ints - """ - duration = self.getDuration() - if duration is None: - return (0, 0) - else: - seconds = int( round(duration / 1000.0) ) - return (seconds / 60, seconds % 60) - - def getPuids(self): - """Returns the PUIDs associated with this track. - - Please note that a PUID may be associated with more than one - track. - - @return: a list of strings, each containing one PUID - """ - return self._puids - - puids = property(getPuids, doc='The list of associated PUIDs.') - - def addPuid(self, puid): - """Add a PUID to this track. - - @param puid: a string containing a PUID - """ - self._puids.append(puid) - - def getISRCs(self): - """Returns the ISRCs associated with this track. - - @return: a list of strings, each containing one ISRC - """ - return self._isrcs - - isrcs = property(getISRCs, doc='The list of associated ISRCs') - - def addISRC(self, isrc): - """Add a ISRC to this track. - - @param isrc: a string containing an ISRC - """ - self._isrcs.append(isrc) - - def getReleases(self): - """Returns the list of releases this track appears on. - - @return: a list of L{Release} objects - """ - return self._releases - - releases = property(getReleases, - doc='The releases on which this track appears.') - - def addRelease(self, release): - """Add a release on which this track appears. - - @param release: a L{Release} object - """ - self._releases.append(release) - - -class Relation(object): - """Represents a relation between two Entities. - - There may be an arbitrary number of relations between all first - class objects in MusicBrainz. The Relation itself has multiple - attributes, which may or may not be used for a given relation - type. - - Note that a L{Relation} object only contains the target but not - the source end of the relation. - - @todo: Add some examples. - - @cvar TO_ARTIST: Identifies relations linking to an artist. - @cvar TO_RELEASE: Identifies relations linking to a release. - @cvar TO_TRACK: Identifies relations linking to a track. - @cvar TO_URL: Identifies relations linking to an URL. - - @cvar DIR_NONE: Relation reading direction doesn't matter. - @cvar DIR_FORWARD: Relation reading direction is from source to target. - @cvar DIR_BACKWARD: Relation reading direction is from target to source. - @cvar DIR_BOTH: Relation reading direction doesn't matter (no longer used!). - """ - # Relation target types - # - TO_ARTIST = NS_REL_1 + 'Artist' - TO_RELEASE = NS_REL_1 + 'Release' - TO_TRACK = NS_REL_1 + 'Track' - TO_URL = NS_REL_1 + 'Url' - - # Relation reading directions - # - DIR_BOTH = 'both' - DIR_FORWARD = 'forward' - DIR_BACKWARD = 'backward' - DIR_NONE = 'none' - - def __init__(self, relationType=None, targetType=None, targetId=None, - direction=DIR_NONE, attributes=None, - beginDate=None, endDate=None, target=None): - """Constructor. - - @param relationType: a string containing an absolute URI - @param targetType: a string containing an absolute URI - @param targetId: a string containing an absolute URI - @param direction: one of C{Relation.DIR_FORWARD}, - C{Relation.DIR_BACKWARD}, or C{Relation.DIR_NONE} - @param attributes: a list of strings containing absolute URIs - @param beginDate: a string containing a date - @param endDate: a string containing a date - @param target: an instance of a subclass of L{Entity} - """ - self._relationType = relationType - self._targetType = targetType - self._targetId = targetId - self._direction = direction - self._beginDate = beginDate - self._endDate = endDate - self._target = target - self._attributes = attributes - if self._attributes is None: - self._attributes = [ ] - - def getType(self): - """Returns this relation's type. - - @return: a string containing an absolute URI, or None - """ - return self._relationType - - def setType(self, type_): - """Sets this relation's type. - - @param type_: a string containing an absolute URI - """ - self._relationType = type_ - - type = property(getType, setType, doc="The relation's type.") - - def getTargetId(self): - """Returns the target's ID. - - This is the ID the relation points to. It is an absolute - URI, and in case of an URL relation, it is a URL. - - @return: a string containing an absolute URI - """ - return self._targetId - - def setTargetId(self, targetId): - """Sets the target's ID. - - @param targetId: a string containing an absolute URI - - @see: L{getTargetId} - """ - self._targetId = targetId - - targetId = property(getTargetId, setTargetId, doc="The target's ID.") - - def getTargetType(self): - """Returns the target's type. - - For MusicBrainz data, the following target types are defined: - - artists: L{Relation.TO_ARTIST} - - releases: L{Relation.TO_RELEASE} - - tracks: L{Relation.TO_TRACK} - - urls: L{Relation.TO_URL} - - @return: a string containing an absolute URI - """ - return self._targetType - - def setTargetType(self, targetType): - """Sets the target's type. - - @param targetType: a string containing an absolute URI - - @see: L{getTargetType} - """ - self._targetType = targetType - - targetId = property(getTargetId, setTargetId, - doc="The type of target this relation points to.") - - def getAttributes(self): - """Returns a list of attributes describing this relation. - - The attributes permitted depend on the relation type. - - @return: a list of strings containing absolute URIs - """ - return self._attributes - - attributes = property(getAttributes, - doc='The list of attributes describing this relation.') - - def addAttribute(self, attribute): - """Adds an attribute to the list. - - @param attribute: a string containing an absolute URI - """ - self._attributes.append(attribute) - - def getBeginDate(self): - """Returns the begin date. - - The definition depends on the relation's type. It may for - example be the day of a marriage or the year an artist - joined a band. For other relation types this may be - undefined. - - @return: a string containing a date - """ - return self._beginDate - - def setBeginDate(self, dateStr): - """Sets the begin date. - - @param dateStr: a string containing a date - - @see: L{getBeginDate} - """ - self._beginDate = dateStr - - beginDate = property(getBeginDate, setBeginDate, doc="The begin date.") - - def getEndDate(self): - """Returns the end date. - - As with the begin date, the definition depends on the - relation's type. Depending on the relation type, this may - or may not be defined. - - @return: a string containing a date - - @see: L{getBeginDate} - """ - return self._endDate - - def setEndDate(self, dateStr): - """Sets the end date. - - @param dateStr: a string containing a date - - @see: L{getBeginDate} - """ - self._endDate = dateStr - - endDate = property(getEndDate, setEndDate, doc="The end date.") - - def getDirection(self): - """Returns the reading direction. - - The direction may be one of L{Relation.DIR_FORWARD}, - L{Relation.DIR_BACKWARD}, or L{Relation.DIR_NONE}, - depending on how the relation should be read. For example, - if direction is L{Relation.DIR_FORWARD} for a cover relation, - it is read as "X is a cover of Y". For some relations there is - no reading direction (like marriages) and the web service doesn't - send a direction. In these cases, the direction is set to - L{Relation.DIR_NONE}. - - @return: L{Relation.DIR_FORWARD}, L{Relation.DIR_BACKWARD}, - or L{Relation.DIR_NONE} - """ - return self._direction - - def setDirection(self, direction): - """Sets the reading direction. - - @param direction: L{Relation.DIR_FORWARD}, - L{Relation.DIR_BACKWARD}, or L{Relation.DIR_NONE} - - @see: L{getDirection} - """ - self._direction = direction - - direction = property(getDirection, setDirection, - doc="The reading direction.") - - def getTarget(self): - """Returns this relation's target object. - - Note that URL relations never have a target object. Use the - L{getTargetId} method to get the URL. - - @return: a subclass of L{Entity}, or None - """ - return self._target - - def setTarget(self, target): - """Sets this relation's target object. - - Note that URL relations never have a target object, they - are set using L{setTargetId}. - - @param target: a subclass of L{Entity} - """ - self._target = target - - target = property(getTarget, setTarget, - doc="The relation's target object.") - - -class ReleaseEvent(object): - """A release event, indicating where and when a release took place. - - All country codes used must be valid ISO-3166 country codes (i.e. 'DE', - 'UK' or 'FR'). The dates are strings and must have the format 'YYYY', - 'YYYY-MM' or 'YYYY-MM-DD'. - - The format of the release medium is a URI that can be compared to the - constants on this class (L{FORMAT_CD}, L{FORMAT_DVD} and others). - """ - FORMAT_CD = NS_MMD_1 + 'CD' - FORMAT_DVD = NS_MMD_1 + 'DVD' - FORMAT_SACD = NS_MMD_1 + 'SACD' - FORMAT_DUALDISC = NS_MMD_1 + 'DualDisc' - FORMAT_LASERDISC = NS_MMD_1 + 'LaserDisc' - FORMAT_MINIDISC = NS_MMD_1 + 'MiniDisc' - FORMAT_VINYL = NS_MMD_1 + 'Vinyl' - FORMAT_CASSETTE = NS_MMD_1 + 'Cassette' - FORMAT_CARTRIDGE = NS_MMD_1 + 'Cartridge' - FORMAT_REEL_TO_REEL = NS_MMD_1 + 'ReelToReel' - FORMAT_DAT = NS_MMD_1 + 'DAT' - FORMAT_DIGITAL = NS_MMD_1 + 'Digital' - FORMAT_WAX_CYLINDER = NS_MMD_1 + 'WaxCylinder' - FORMAT_PIANO_ROLL = NS_MMD_1 + 'PianoRoll' - FORMAT_OTHER = NS_MMD_1 + 'Other' - - def __init__(self, country=None, dateStr=None): - """Constructor. - - @param country: a string containing an ISO-3166 country code - @param dateStr: a string containing a date string - """ - self._countryId = country - self._dateStr = dateStr - self._catalogNumber = None - self._barcode = None - self._label = None - self._format = None - - def getCountry(self): - """Returns the country a release took place. - - @note: Due to a server limitation, the web service does not - return country IDs for release collection queries. This only - affects the L{musicbrainz2.webservice.Query.getReleases} query. - - @return: a string containing an ISO-3166 country code, or None - - @see: L{musicbrainz2.utils.getCountryName} - """ - return self._countryId - - def setCountry(self, country): - """Sets the country a release took place. - - @param country: a string containing an ISO-3166 country code - """ - self._countryId = country - - country = property(getCountry, setCountry, - doc='The country a release took place.') - - def getCatalogNumber(self): - """Returns the catalog number of this release event. - - @return: A string containing the catalog number, or None - """ - return self._catalogNumber - - def setCatalogNumber(self, catalogNumber): - """Sets the catalog number of this release event. - - @param catalogNumber: A string containing the catalog number - """ - self._catalogNumber = catalogNumber - - catalogNumber = property(getCatalogNumber, setCatalogNumber, - doc='The catalog number of the release event') - - def getBarcode(self): - """Returns the barcode of this release event. - - @return: A string containing the barcode, or None - """ - return self._barcode - - def setBarcode(self, barcode): - """Sets the barcode of this release event. - - @param barcode: A string containing the barcode - """ - self._barcode = barcode - - barcode = property(getBarcode, setBarcode, - doc='The barcode of the release event') - - def getLabel(self): - """Returns a L{Label} object for the label associated with this release. - - @return: a L{Label} object, or None - """ - return self._label - - def setLabel(self, label): - """Sets the label of this release event. - - @param label: A L{Label} object - """ - self._label = label - - label = property(getLabel, setLabel, doc='The label of the release') - - def getDate(self): - """Returns the date a release took place. - - @return: a string containing a date - """ - return self._dateStr - - def setDate(self, dateStr): - """Sets the date a release took place. - - @param dateStr: a string containing a date - """ - self._dateStr = dateStr - - date = property(getDate, setDate, doc='The date a release took place.') - - def getFormat(self): - """Returns the format of the release medium. - - @return: a string containing a URI, or None - """ - return self._format - - def setFormat(self, format): - """Sets the format of the release medium. - - @param format: a string containing a URI - """ - self._format = format - - format = property(getFormat, setFormat, - doc='The format of the release medium.') - - -class CDStub(object): - """Represents a CD Stub""" - - def __init__(self, disc): - """Constructor. - - @param disc: a L{Disc} object to create this CD Stub from - """ - assert isinstance(disc, Disc), 'musicbrainz2.model.Disc expected' - self._disc = disc - self._tracks = [ ] - self._title = "" - self._artist = "" - self._barcode = "" - self._comment = "" - - def setTitle(self, title): - """Sets the title of this release. - - @param title: a string containing the title - """ - self._title = title - - def getTitle(self): - """Returns the title of this release. - - @return: a string containing the title - """ - return self._title - - title = property(getTitle, setTitle, - doc='The title of the release') - - def setArtist(self, artist): - """Sets the artist of this release. - - @param artist: a string containing the artist - """ - self._artist = artist - - def getArtist(self): - """Returns the artist of this release. - - @return: a string containing the artist - """ - return self._artist - - artist = property(getArtist, setArtist, - doc='The artist of the release') - - def setComment(self, comment): - """Sets the comment for this release. - - @param comment: a string containing the comment - """ - self._comment = comment - - def getComment(self): - """Returns the comment for this release. - - @return: a string containing the comment - """ - return self._comment - - comment = property(getComment, setComment, - doc='Comment for the release (optional)') - - def setBarcode(self, barcode): - """Sets the barcode of this release. - - @param barcode: a string containing the barcode - """ - self._barcode = barcode - - def getBarcode(self): - """Returns the barcode of this release. - - @return: a string containing the barcode - """ - return self._barcode - - barcode = property(getBarcode, setBarcode, - doc='Barcode for the release (optional)') - - def addTrack(self, title, artist=''): - """Add a track to this release - - @param title: a string containing the title of the track - @param artist: a string containing the artist of the track, - if different to the album artist - """ - self._tracks.append((title, artist)) - - def getTracks(self): - """Return all the tracks on the release. - - @return: a list of tuples containing (title, artist) pairs - for each track - """ - return self._tracks - - tracks = property(getTracks, doc='The tracks of the release.') - -class Disc(object): - """Represents an Audio CD. - - This class represents an Audio CD. A disc can have an ID (the - MusicBrainz DiscID), which is calculated from the CD's table of - contents (TOC). There may also be data from the TOC like the length - of the disc in sectors, as well as position and length of the tracks. - - Note that different TOCs, maybe due to different pressings, lead to - different DiscIDs. Conversely, if two different discs have the same - TOC, they also have the same DiscID (which is unlikely but not - impossible). DiscIDs are always 28 characters long and look like this: - C{'J68I_CDcUFdCRCIbHSEbTBCbooA-'}. Sometimes they are also referred - to as CDIndex IDs. - - The L{MusicBrainz web service } only returns - the DiscID and the number of sectors. The DiscID calculation function - L{musicbrainz2.disc.readDisc}, however, can retrieve the other - attributes of L{Disc} from an Audio CD in the disc drive. - """ - def __init__(self, id_=None): - """Constructor. - - @param id_: a string containing a 28-character DiscID - """ - self._id = id_ - self._sectors = None - self._firstTrackNum = None - self._lastTrackNum = None - self._tracks = [ ] - - def getId(self): - """Returns the MusicBrainz DiscID. - - @return: a string containing a 28-character DiscID - """ - return self._id - - def setId(self, id_): - """Sets the MusicBrainz DiscId. - - @param id_: a string containing a 28-character DiscID - """ - self._id = id_ - - id = property(getId, setId, doc="The MusicBrainz DiscID.") - - def getSectors(self): - """Returns the length of the disc in sectors. - - @return: the length in sectors as an integer, or None - """ - return self._sectors - - def setSectors(self, sectors): - """Sets the length of the disc in sectors. - - @param sectors: the length in sectors as an integer - """ - self._sectors = sectors - - sectors = property(getSectors, setSectors, - doc="The length of the disc in sectors.") - - def getFirstTrackNum(self): - """Returns the number of the first track on this disc. - - @return: an int containing the track number, or None - """ - return self._firstTrackNum - - def setFirstTrackNum(self, trackNum): - """Sets the number of the first track on this disc. - - @param trackNum: an int containing the track number, or None - """ - self._firstTrackNum = trackNum - - firstTrackNum = property(getFirstTrackNum, setFirstTrackNum, - doc="The number of the first track on this disc.") - - def getLastTrackNum(self): - """Returns the number of the last track on this disc. - - @return: an int containing the track number, or None - """ - return self._lastTrackNum - - def setLastTrackNum(self, trackNum): - """Sets the number of the last track on this disc. - - @param trackNum: an int containing the track number, or None - """ - self._lastTrackNum = trackNum - - lastTrackNum = property(getLastTrackNum, setLastTrackNum, - doc="The number of the last track on this disc.") - - def getTracks(self): - """Returns the sector offset and length of this disc. - - This method returns a list of tuples containing the track - offset and length in sectors for all tracks on this disc. - The track offset is measured from the beginning of the disc, - the length is relative to the track's offset. Note that the - leadout track is I{not} included. - - @return: a list of (offset, length) tuples (values are ints) - """ - return self._tracks - - tracks = property(getTracks, - doc='Sector offset and length of all tracks.') - - def addTrack(self, track): - """Adds a track to the list. - - This method adds an (offset, length) tuple to the list of - tracks. The leadout track must I{not} be added. The total - length of the disc can be set using L{setSectors}. - - @param track: an (offset, length) tuple (values are ints) - - @see: L{getTracks} - """ - self._tracks.append(track) - - -class AbstractAlias(object): - """An abstract super class for all alias classes.""" - def __init__(self, value=None, type_=None, script=None): - """Constructor. - - @param value: a string containing the alias - @param type_: a string containing an absolute URI - @param script: a string containing an ISO-15924 script code - """ - self._value = value - self._type = type_ - self._script = script - - def getValue(self): - """Returns the alias. - - @return: a string containing the alias - """ - return self._value - - def setValue(self, value): - """Sets the alias. - - @param value: a string containing the alias - """ - self._value = value - - value = property(getValue, setValue, doc='The alias value.') - - def getType(self): - """Returns the alias type. - - @return: a string containing an absolute URI, or None - """ - return self._type - - def setType(self, type_): - """Sets the alias type. - - @param type_: a string containing an absolute URI, or None - """ - self._type = type_ - - type = property(getType, setType, doc='The alias type.') - - def getScript(self): - """Returns the alias script. - - @return: a string containing an ISO-15924 script code - """ - return self._script - - def setScript(self, script): - """Sets the alias script. - - @param script: a string containing an ISO-15924 script code - """ - self._script = script - - script = property(getScript, setScript, doc='The alias script.') - - -class ArtistAlias(AbstractAlias): - """Represents an artist alias. - - An alias (the I{alias value}) is a different representation of an - artist's name. This may be a common misspelling or a transliteration - (the I{alias type}). - - The I{alias script} is interesting mostly for transliterations and - indicates which script is used for the alias value. To represent the - script, ISO-15924 script codes like 'Latn', 'Cyrl', or 'Hebr' are used. - """ - pass - - -class LabelAlias(AbstractAlias): - """Represents a label alias. - - An alias (the I{alias value}) is a different representation of a - label's name. This may be a common misspelling or a transliteration - (the I{alias type}). - - The I{alias script} is interesting mostly for transliterations and - indicates which script is used for the alias value. To represent the - script, ISO-15924 script codes like 'Latn', 'Cyrl', or 'Hebr' are used. - """ - pass - - -class User(object): - """Represents a MusicBrainz user.""" - - def __init__(self): - """Constructor.""" - self._name = None - self._types = [ ] - self._showNag = None - - def getName(self): - """Returns the user name. - - @return: a string containing the user name - """ - return self._name - - def setName(self, name): - """Sets the user name. - - @param name: a string containing the user name - """ - self._name = name - - name = property(getName, setName, doc='The MusicBrainz user name.') - - def getTypes(self): - """Returns the types of this user. - - Most users' type list is empty. Currently, the following types - are defined: - - - 'http://musicbrainz.org/ns/ext-1.0#AutoEditor' - - 'http://musicbrainz.org/ns/ext-1.0#RelationshipEditor' - - 'http://musicbrainz.org/ns/ext-1.0#Bot' - - 'http://musicbrainz.org/ns/ext-1.0#NotNaggable' - - @return: a list of strings containing absolute URIs - """ - return self._types - - types = property(getTypes, doc="The user's types.") - - def addType(self, type_): - """Add a type to the list of types. - - @param type_: a string containing absolute URIs - - @see: L{getTypes} - """ - self._types.append(type_) - - def getShowNag(self): - """Returns true if a nag screen should be displayed to the user. - - @return: C{True}, C{False}, or None - """ - return self._showNag - - def setShowNag(self, value): - """Sets the value of the nag screen flag. - - If set to C{True}, - - @param value: C{True} or C{False} - - @see: L{getShowNag} - """ - self._showNag = value - - showNag = property(getShowNag, setShowNag, - doc='The value of the nag screen flag.') - -# EOF diff --git a/lib/musicbrainz2/utils.py b/lib/musicbrainz2/utils.py deleted file mode 100644 index 644ad9fe..00000000 --- a/lib/musicbrainz2/utils.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Various utilities to simplify common tasks. - -This module contains helper functions to make common tasks easier. - -@author: Matthias Friedrich -""" -__revision__ = '$Id: utils.py 11853 2009-07-21 09:26:50Z luks $' - -import re -import urlparse -import os.path - -__all__ = [ - 'extractUuid', 'extractFragment', 'extractEntityType', - 'getReleaseTypeName', 'getCountryName', 'getLanguageName', - 'getScriptName', -] - - -# A pattern to split the path part of an absolute MB URI. -PATH_PATTERN = '^/(artist|release|track|label|release-group)/([^/]*)$' - - -def extractUuid(uriStr, resType=None): - """Extract the UUID part from a MusicBrainz identifier. - - This function takes a MusicBrainz ID (an absolute URI) as the input - and returns the UUID part of the URI, thus turning it into a relative - URI. If C{uriStr} is None or a relative URI, then it is returned - unchanged. - - The C{resType} parameter can be used for error checking. Set it to - 'artist', 'release', or 'track' to make sure C{uriStr} is a - syntactically valid MusicBrainz identifier of the given resource - type. If it isn't, a C{ValueError} exception is raised. - This error checking only works if C{uriStr} is an absolute URI, of - course. - - Example: - - >>> from musicbrainz2.utils import extractUuid - >>> extractUuid('http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5', 'artist') - 'c0b2500e-0cef-4130-869d-732b23ed9df5' - >>> - - @param uriStr: a string containing a MusicBrainz ID (an URI), or None - @param resType: a string containing a resource type - - @return: a string containing a relative URI, or None - - @raise ValueError: the given URI is no valid MusicBrainz ID - """ - if uriStr is None: - return None - - (scheme, netloc, path) = urlparse.urlparse(uriStr)[:3] - - if scheme == '': - return uriStr # no URI, probably already the UUID - - if scheme != 'http' or netloc != 'musicbrainz.org': - raise ValueError('%s is no MB ID.' % uriStr) - - m = re.match(PATH_PATTERN, path) - - if m: - if resType is None: - return m.group(2) - else: - if m.group(1) == resType: - return m.group(2) - else: - raise ValueError('expected "%s" Id' % resType) - else: - raise ValueError('%s is no valid MB ID.' % uriStr) - - -def extractFragment(uriStr, uriPrefix=None): - """Extract the fragment part from a URI. - - If C{uriStr} is None or no absolute URI, then it is returned unchanged. - - The C{uriPrefix} parameter can be used for error checking. If C{uriStr} - is an absolute URI, then the function checks if it starts with - C{uriPrefix}. If it doesn't, a C{ValueError} exception is raised. - - @param uriStr: a string containing an absolute URI - @param uriPrefix: a string containing an URI prefix - - @return: a string containing the fragment, or None - - @raise ValueError: the given URI doesn't start with C{uriPrefix} - """ - if uriStr is None: - return None - - (scheme, netloc, path, params, query, frag) = urlparse.urlparse(uriStr) - if scheme == '': - return uriStr # this is no URI - - if uriPrefix is None or uriStr.startswith(uriPrefix): - return frag - else: - raise ValueError("prefix doesn't match URI %s" % uriStr) - - -def extractEntityType(uriStr): - """Returns the entity type an entity URI is referring to. - - @param uriStr: a string containing an absolute entity URI - - @return: a string containing 'artist', 'release', 'track', or 'label' - - @raise ValueError: if the given URI is no valid MusicBrainz ID - """ - if uriStr is None: - raise ValueError('None is no valid entity URI') - - (scheme, netloc, path) = urlparse.urlparse(uriStr)[:3] - - if scheme == '': - raise ValueError('%s is no absolute MB ID.' % uriStr) - - if scheme != 'http' or netloc != 'musicbrainz.org': - raise ValueError('%s is no MB ID.' % uriStr) - - m = re.match(PATH_PATTERN, path) - - if m: - return m.group(1) - else: - raise ValueError('%s is no valid MB ID.' % uriStr) - - -def getReleaseTypeName(releaseType): - """Returns the name of a release type URI. - - @param releaseType: a string containing a release type URI - - @return: a string containing a printable name for the release type - - @see: L{musicbrainz2.model.Release} - """ - from lib.musicbrainz2.data.releasetypenames import releaseTypeNames - return releaseTypeNames.get(releaseType) - - -def getCountryName(id_): - """Returns a country's name based on an ISO-3166 country code. - - The country table this function is based on has been modified for - MusicBrainz purposes by using the extension mechanism defined in - ISO-3166. All IDs are still valid ISO-3166 country codes, but some - IDs have been added to include historic countries and some of the - country names have been modified to make them better suited for - display purposes. - - If the country ID is not found, None is returned. This may happen - for example, when new countries are added to the MusicBrainz web - service which aren't known to this library yet. - - @param id_: a two-letter upper case string containing an ISO-3166 code - - @return: a string containing the country's name, or None - - @see: L{musicbrainz2.model} - """ - from musicbrainz2.data.countrynames import countryNames - return countryNames.get(id_) - - -def getLanguageName(id_): - """Returns a language name based on an ISO-639-2/T code. - - This function uses a subset of the ISO-639-2/T code table to map - language IDs (terminologic, not bibliographic ones!) to names. - - @param id_: a three-letter upper case string containing an ISO-639-2/T code - - @return: a string containing the language's name, or None - - @see: L{musicbrainz2.model} - """ - from musicbrainz2.data.languagenames import languageNames - return languageNames.get(id_) - - -def getScriptName(id_): - """Returns a script name based on an ISO-15924 code. - - This function uses a subset of the ISO-15924 code table to map - script IDs to names. - - @param id_: a four-letter string containing an ISO-15924 script code - - @return: a string containing the script's name, or None - - @see: L{musicbrainz2.model} - """ - from musicbrainz2.data.scriptnames import scriptNames - return scriptNames.get(id_) - - -# EOF diff --git a/lib/musicbrainz2/webservice.py b/lib/musicbrainz2/webservice.py deleted file mode 100644 index c6cc5c99..00000000 --- a/lib/musicbrainz2/webservice.py +++ /dev/null @@ -1,1524 +0,0 @@ -"""Classes for interacting with the MusicBrainz XML web service. - -The L{WebService} class talks to a server implementing the MusicBrainz XML -web service. It mainly handles URL generation and network I/O. Use this -if maximum control is needed. - -The L{Query} class provides a convenient interface to the most commonly -used features of the web service. By default it uses L{WebService} to -retrieve data and the L{XML parser } to parse the -responses. The results are object trees using the L{MusicBrainz domain -model }. - -@author: Matthias Friedrich -""" -__revision__ = '$Id: webservice.py 12973 2011-04-29 11:49:31Z luks $' - -import re -import urllib -import urllib2 -import base64 -import urlparse -import logging -import os.path -from StringIO import StringIO -import lib.musicbrainz2 as musicbrainz2 -from lib.musicbrainz2.model import Artist, Release, Track -from lib.musicbrainz2.wsxml import MbXmlParser, ParseError -import lib.musicbrainz2.utils as mbutils - -__all__ = [ - 'WebServiceError', 'AuthenticationError', 'ConnectionError', - 'RequestError', 'ResourceNotFoundError', 'ResponseError', - 'IIncludes', 'ArtistIncludes', 'ReleaseIncludes', 'TrackIncludes', - 'LabelIncludes', 'ReleaseGroupIncludes', - 'IFilter', 'ArtistFilter', 'ReleaseFilter', 'TrackFilter', - 'UserFilter', 'LabelFilter', 'ReleaseGroupFilter', - 'IWebService', 'WebService', 'Query', -] - - -class IWebService(object): - """An interface all concrete web service classes have to implement. - - All web service classes have to implement this and follow the - method specifications. - """ - - def get(self, entity, id_, include, filter, version): - """Query the web service. - - Using this method, you can either get a resource by id (using - the C{id_} parameter, or perform a query on all resources of - a type. - - The C{filter} and the C{id_} parameter exclude each other. If - you are using a filter, you may not set C{id_} and vice versa. - - Returns a file-like object containing the result or raises a - L{WebServiceError} or one of its subclasses in case of an - error. Which one is used depends on the implementing class. - - @param entity: a string containing the entity's name - @param id_: a string containing a UUID, or the empty string - @param include: a tuple containing values for the 'inc' parameter - @param filter: parameters, depending on the entity - @param version: a string containing the web service version to use - - @return: a file-like object - - @raise WebServiceError: in case of errors - """ - raise NotImplementedError() - - - def post(self, entity, id_, data, version): - """Submit data to the web service. - - @param entity: a string containing the entity's name - @param id_: a string containing a UUID, or the empty string - @param data: A string containing the data to post - @param version: a string containing the web service version to use - - @return: a file-like object - - @raise WebServiceError: in case of errors - """ - raise NotImplementedError() - - -class WebServiceError(Exception): - """A web service error has occurred. - - This is the base class for several other web service related - exceptions. - """ - - def __init__(self, msg='Webservice Error', reason=None): - """Constructor. - - Set C{msg} to an error message which explains why this - exception was raised. The C{reason} parameter should be the - original exception which caused this L{WebService} exception - to be raised. If given, it has to be an instance of - C{Exception} or one of its child classes. - - @param msg: a string containing an error message - @param reason: another exception instance, or None - """ - Exception.__init__(self) - self.msg = msg - self.reason = reason - - def __str__(self): - """Makes this class printable. - - @return: a string containing an error message - """ - return self.msg - - -class ConnectionError(WebServiceError): - """Getting a server connection failed. - - This exception is mostly used if the client couldn't connect to - the server because of an invalid host name or port. It doesn't - make sense if the web service in question doesn't use the network. - """ - pass - - -class RequestError(WebServiceError): - """An invalid request was made. - - This exception is raised if the client made an invalid request. - That could be syntactically invalid identifiers or unknown or - invalid parameter values. - """ - pass - - -class ResourceNotFoundError(WebServiceError): - """No resource with the given ID exists. - - This is usually a wrapper around IOError (which is superclass of - HTTPError). - """ - pass - - -class AuthenticationError(WebServiceError): - """Authentication failed. - - This is thrown if user name, password or realm were invalid while - trying to access a protected resource. - """ - pass - - -class ResponseError(WebServiceError): - """The returned resource was invalid. - - This may be due to a malformed XML document or if the requested - data wasn't part of the response. It can only occur in case of - bugs in the web service itself. - """ - pass - -class DigestAuthHandler(urllib2.HTTPDigestAuthHandler): - """Patched DigestAuthHandler to correctly handle Digest Auth according to RFC 2617. - - This will allow multiple qop values in the WWW-Authenticate header (e.g. "auth,auth-int"). - The only supported qop value is still auth, though. - See http://bugs.python.org/issue9714 - - @author Kuno Woudt - """ - def get_authorization(self, req, chal): - qop = chal.get('qop') - if qop and ',' in qop and 'auth' in qop.split(','): - chal['qop'] = 'auth' - - return urllib2.HTTPDigestAuthHandler.get_authorization(self, req, chal) - -class WebService(IWebService): - """An interface to the MusicBrainz XML web service via HTTP. - - By default, this class uses the MusicBrainz server but may be - configured for accessing other servers as well using the - L{constructor <__init__>}. This implements L{IWebService}, so - additional documentation on method parameters can be found there. - """ - - def __init__(self, host='musicbrainz.org', port=80, pathPrefix='/ws', - username=None, password=None, realm='musicbrainz.org', - opener=None, mirror=None): - """Constructor. - - This can be used without parameters. In this case, the - MusicBrainz server will be used. - - @param host: a string containing a host name - @param port: an integer containing a port number - @param pathPrefix: a string prepended to all URLs - @param username: a string containing a MusicBrainz user name - @param password: a string containing the user's password - @param realm: a string containing the realm used for authentication - @param opener: an C{urllib2.OpenerDirector} object used for queries - """ - self._host = host - self._port = port - self._username = username - self._password = password - self._realm = realm - self._pathPrefix = pathPrefix - self._log = logging.getLogger(str(self.__class__)) - self._mirror = mirror - - if opener is None: - self._opener = urllib2.build_opener() - else: - self._opener = opener - - passwordMgr = self._RedirectPasswordMgr() - authHandler = DigestAuthHandler(passwordMgr) - authHandler.add_password(self._realm, (), # no host set - self._username, self._password) - self._opener.add_handler(authHandler) - - - def _makeUrl(self, entity, id_, include=( ), filter={ }, - version='1', type_='xml'): - params = dict(filter) - if type_ is not None: - params['type'] = type_ - if len(include) > 0: - params['inc'] = ' '.join(include) - - netloc = self._host - if self._port != 80: - netloc += ':' + str(self._port) - path = '/'.join((self._pathPrefix, version, entity, id_)) - - query = urllib.urlencode(params) - - url = urlparse.urlunparse(('http', netloc, path, '', query,'')) - - return url - - - def _openUrl(self, url, data=None): - userAgent = 'python-headphones/' + musicbrainz2.__version__ - req = urllib2.Request(url) - req.add_header('User-Agent', userAgent) - if self._mirror == 'headphones': - base64string = base64.encodestring('%s:%s' % (self._username, self._password)).replace('\n', '') - req.add_header("Authorization", "Basic %s" % base64string) - return self._opener.open(req, data) - - - def get(self, entity, id_, include=( ), filter={ }, version='1'): - """Query the web service via HTTP-GET. - - Returns a file-like object containing the result or raises a - L{WebServiceError}. Conditions leading to errors may be - invalid entities, IDs, C{include} or C{filter} parameters - and unsupported version numbers. - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid IDs or parameters - @raise AuthenticationError: invalid user name and/or password - @raise ResourceNotFoundError: resource doesn't exist - - @see: L{IWebService.get} - """ - url = self._makeUrl(entity, id_, include, filter, version) - - self._log.debug('GET ' + url) - - try: - return self._openUrl(url) - except urllib2.HTTPError, e: - self._log.debug("GET failed: " + str(e)) - if e.code == 400: # in python 2.4: httplib.BAD_REQUEST - raise RequestError(str(e), e) - elif e.code == 401: # httplib.UNAUTHORIZED - raise AuthenticationError(str(e), e) - elif e.code == 404: # httplib.NOT_FOUND - raise ResourceNotFoundError(str(e), e) - else: - raise WebServiceError(str(e), e) - except urllib2.URLError, e: - self._log.debug("GET failed: " + str(e)) - raise ConnectionError(str(e), e) - - - def post(self, entity, id_, data, version='1'): - """Send data to the web service via HTTP-POST. - - Note that this may require authentication. You can set - user name, password and realm in the L{constructor <__init__>}. - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid IDs or parameters - @raise AuthenticationError: invalid user name and/or password - @raise ResourceNotFoundError: resource doesn't exist - - @see: L{IWebService.post} - """ - url = self._makeUrl(entity, id_, version=version, type_=None) - - self._log.debug('POST ' + url) - self._log.debug('POST-BODY: ' + data) - - try: - return self._openUrl(url, data) - except urllib2.HTTPError, e: - self._log.debug("POST failed: " + str(e)) - if e.code == 400: # in python 2.4: httplib.BAD_REQUEST - raise RequestError(str(e), e) - elif e.code == 401: # httplib.UNAUTHORIZED - raise AuthenticationError(str(e), e) - elif e.code == 404: # httplib.NOT_FOUND - raise ResourceNotFoundError(str(e), e) - else: - raise WebServiceError(str(e), e) - except urllib2.URLError, e: - self._log.debug("POST failed: " + str(e)) - raise ConnectionError(str(e), e) - - - # Special password manager which also works with redirects by simply - # ignoring the URI. As a consequence, only *ONE* (username, password) - # tuple per realm can be used for all URIs. - # - class _RedirectPasswordMgr(urllib2.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 add_password(self, realm, uri, username, password): - # ignoring the uri parameter intentionally - self._realms[realm] = (username, password) - - -class IFilter(object): - """A filter for collections. - - This is the interface all filters have to implement. Filter classes - are initialized with a set of criteria and are then applied to - collections of items. The criteria are usually strings or integer - values, depending on the filter. - - Note that all strings passed to filters should be unicode strings - (python type C{unicode}). Standard strings are converted to unicode - internally, but have a limitation: Only 7 Bit pure ASCII characters - may be used, otherwise a C{UnicodeDecodeError} is raised. - """ - def createParameters(self): - """Create a list of query parameters. - - This method creates a list of (C{parameter}, C{value}) tuples, - based on the contents of the implementing subclass. - C{parameter} is a string containing a parameter name - and C{value} an arbitrary string. No escaping of those strings - is required. - - @return: a sequence of (key, value) pairs - """ - raise NotImplementedError() - - -class ArtistFilter(IFilter): - """A filter for the artist collection.""" - - def __init__(self, name=None, limit=None, offset=None, query=None): - """Constructor. - - The C{query} parameter may contain a query in U{Lucene syntax - }. - Note that the C{name} and C{query} may not be used together. - - @param name: a unicode string containing the artist's name - @param limit: the maximum number of artists to return - @param offset: start results at this zero-based offset - @param query: a string containing a query in Lucene syntax - """ - self._params = [ - ('name', name), - ('limit', limit), - ('offset', offset), - ('query', query), - ] - - if not _paramsValid(self._params): - raise ValueError('invalid combination of parameters') - - def createParameters(self): - return _createParameters(self._params) - - -class LabelFilter(IFilter): - """A filter for the label collection.""" - - def __init__(self, name=None, limit=None, offset=None, query=None): - """Constructor. - - The C{query} parameter may contain a query in U{Lucene syntax - }. - Note that the C{name} and C{query} may not be used together. - - @param name: a unicode string containing the label's name - @param limit: the maximum number of labels to return - @param offset: start results at this zero-based offset - @param query: a string containing a query in Lucene syntax - """ - self._params = [ - ('name', name), - ('limit', limit), - ('offset', offset), - ('query', query), - ] - - if not _paramsValid(self._params): - raise ValueError('invalid combination of parameters') - - def createParameters(self): - return _createParameters(self._params) - -class ReleaseGroupFilter(IFilter): - """A filter for the release group collection.""" - - def __init__(self, title=None, releaseTypes=None, artistName=None, - artistId=None, limit=None, offset=None, query=None): - """Constructor. - - If C{artistId} is set, only releases matching those IDs are - returned. The C{releaseTypes} parameter allows you to limit - the types of the release groups returned. You can set it to - C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example, - to only get officially released albums. Note that those values - are connected using the I{AND} operator. MusicBrainz' support - is currently very limited, so C{Release.TYPE_LIVE} and - C{Release.TYPE_COMPILATION} exclude each other (see U{the - documentation on release attributes - } for more - information and all valid values). - - If both the C{artistName} and the C{artistId} parameter are - given, the server will ignore C{artistName}. - - The C{query} parameter may contain a query in U{Lucene syntax - }. - Note that C{query} may not be used together with the other - parameters except for C{limit} and C{offset}. - - @param title: a unicode string containing the release group's title - @param releaseTypes: a sequence of release type URIs - @param artistName: a unicode string containing the artist's name - @param artistId: a unicode string containing the artist's ID - @param limit: the maximum number of release groups to return - @param offset: start results at this zero-based offset - @param query: a string containing a query in Lucene syntax - - @see: the constants in L{musicbrainz2.model.Release} - """ - if releaseTypes is None or len(releaseTypes) == 0: - releaseTypesStr = None - else: - releaseTypesStr = ' '.join(map(mbutils.extractFragment, releaseTypes)) - - self._params = [ - ('title', title), - ('releasetypes', releaseTypesStr), - ('artist', artistName), - ('artistid', mbutils.extractUuid(artistId)), - ('limit', limit), - ('offset', offset), - ('query', query), - ] - - if not _paramsValid(self._params): - raise ValueError('invalid combination of parameters') - - def createParameters(self): - return _createParameters(self._params) - - -class ReleaseFilter(IFilter): - """A filter for the release collection.""" - - def __init__(self, title=None, discId=None, releaseTypes=None, - artistName=None, artistId=None, limit=None, - offset=None, query=None, trackCount=None): - """Constructor. - - If C{discId} or C{artistId} are set, only releases matching - those IDs are returned. The C{releaseTypes} parameter allows - to limit the types of the releases returned. You can set it to - C{(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)}, for example, - to only get officially released albums. Note that those values - are connected using the I{AND} operator. MusicBrainz' support - is currently very limited, so C{Release.TYPE_LIVE} and - C{Release.TYPE_COMPILATION} exclude each other (see U{the - documentation on release attributes - } for more - information and all valid values). - - If both the C{artistName} and the C{artistId} parameter are - given, the server will ignore C{artistName}. - - The C{query} parameter may contain a query in U{Lucene syntax - }. - Note that C{query} may not be used together with the other - parameters except for C{limit} and C{offset}. - - @param title: a unicode string containing the release's title - @param discId: a unicode string containing the DiscID - @param releaseTypes: a sequence of release type URIs - @param artistName: a unicode string containing the artist's name - @param artistId: a unicode string containing the artist's ID - @param limit: the maximum number of releases to return - @param offset: start results at this zero-based offset - @param query: a string containing a query in Lucene syntax - @param trackCount: the number of tracks in the release - - @see: the constants in L{musicbrainz2.model.Release} - """ - if releaseTypes is None or len(releaseTypes) == 0: - releaseTypesStr = None - else: - tmp = [ mbutils.extractFragment(x) for x in releaseTypes ] - releaseTypesStr = ' '.join(tmp) - - self._params = [ - ('title', title), - ('discid', discId), - ('releasetypes', releaseTypesStr), - ('artist', artistName), - ('artistid', mbutils.extractUuid(artistId)), - ('limit', limit), - ('offset', offset), - ('query', query), - ('count', trackCount), - ] - - if not _paramsValid(self._params): - raise ValueError('invalid combination of parameters') - - def createParameters(self): - return _createParameters(self._params) - - -class TrackFilter(IFilter): - """A filter for the track collection.""" - - def __init__(self, title=None, artistName=None, artistId=None, - releaseTitle=None, releaseId=None, - duration=None, puid=None, limit=None, offset=None, - query=None): - """Constructor. - - If C{artistId}, C{releaseId} or C{puid} are set, only tracks - matching those IDs are returned. - - The server will ignore C{artistName} and C{releaseTitle} if - C{artistId} or ${releaseId} are set respectively. - - The C{query} parameter may contain a query in U{Lucene syntax - }. - Note that C{query} may not be used together with the other - parameters except for C{limit} and C{offset}. - - @param title: a unicode string containing the track's title - @param artistName: a unicode string containing the artist's name - @param artistId: a string containing the artist's ID - @param releaseTitle: a unicode string containing the release's title - @param releaseId: a string containing the release's title - @param duration: the track's length in milliseconds - @param puid: a string containing a PUID - @param limit: the maximum number of releases to return - @param offset: start results at this zero-based offset - @param query: a string containing a query in Lucene syntax - """ - self._params = [ - ('title', title), - ('artist', artistName), - ('artistid', mbutils.extractUuid(artistId)), - ('release', releaseTitle), - ('releaseid', mbutils.extractUuid(releaseId)), - ('duration', duration), - ('puid', puid), - ('limit', limit), - ('offset', offset), - ('query', query), - ] - - if not _paramsValid(self._params): - raise ValueError('invalid combination of parameters') - - def createParameters(self): - return _createParameters(self._params) - - -class UserFilter(IFilter): - """A filter for the user collection.""" - - def __init__(self, name=None): - """Constructor. - - @param name: a unicode string containing a MusicBrainz user name - """ - self._name = name - - def createParameters(self): - if self._name is not None: - return [ ('name', self._name.encode('utf-8')) ] - else: - return [ ] - - -class IIncludes(object): - """An interface implemented by include tag generators.""" - def createIncludeTags(self): - raise NotImplementedError() - - -class ArtistIncludes(IIncludes): - """A specification on how much data to return with an artist. - - Example: - - >>> from musicbrainz2.model import Release - >>> from musicbrainz2.webservice import ArtistIncludes - >>> inc = ArtistIncludes(artistRelations=True, releaseRelations=True, - ... releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)) - >>> - - The MusicBrainz server only supports some combinations of release - types for the C{releases} and C{vaReleases} include tags. At the - moment, not more than two release types should be selected, while - one of them has to be C{Release.TYPE_OFFICIAL}, - C{Release.TYPE_PROMOTION} or C{Release.TYPE_BOOTLEG}. - - @note: Only one of C{releases} and C{vaReleases} may be given. - """ - def __init__(self, aliases=False, releases=(), vaReleases=(), - artistRelations=False, releaseRelations=False, - trackRelations=False, urlRelations=False, tags=False, - ratings=False, releaseGroups=False): - - assert not isinstance(releases, basestring) - assert not isinstance(vaReleases, basestring) - assert len(releases) == 0 or len(vaReleases) == 0 - - self._includes = { - 'aliases': aliases, - 'artist-rels': artistRelations, - 'release-groups': releaseGroups, - 'release-rels': releaseRelations, - 'track-rels': trackRelations, - 'url-rels': urlRelations, - 'tags': tags, - 'ratings': ratings, - } - - for elem in releases: - self._includes['sa-' + mbutils.extractFragment(elem)] = True - - for elem in vaReleases: - self._includes['va-' + mbutils.extractFragment(elem)] = True - - def createIncludeTags(self): - return _createIncludes(self._includes) - - -class ReleaseIncludes(IIncludes): - """A specification on how much data to return with a release.""" - def __init__(self, artist=False, counts=False, releaseEvents=False, - discs=False, tracks=False, - artistRelations=False, releaseRelations=False, - trackRelations=False, urlRelations=False, - labels=False, tags=False, ratings=False, isrcs=False, - releaseGroup=False): - self._includes = { - 'artist': artist, - 'counts': counts, - 'labels': labels, - 'release-groups': releaseGroup, - 'release-events': releaseEvents, - 'discs': discs, - 'tracks': tracks, - 'artist-rels': artistRelations, - 'release-rels': releaseRelations, - 'track-rels': trackRelations, - 'url-rels': urlRelations, - 'tags': tags, - 'ratings': ratings, - 'isrcs': isrcs, - } - - # Requesting labels without releaseEvents makes no sense, - # so we pull in releaseEvents, if necessary. - if labels and not releaseEvents: - self._includes['release-events'] = True - # Ditto for isrcs with no tracks - if isrcs and not tracks: - self._includes['tracks'] = True - - def createIncludeTags(self): - return _createIncludes(self._includes) - - -class ReleaseGroupIncludes(IIncludes): - """A specification on how much data to return with a release group.""" - - def __init__(self, artist=False, releases=False, tags=False): - """Constructor. - - @param artist: Whether to include the release group's main artist info. - @param releases: Whether to include the release group's releases. - """ - self._includes = { - 'artist': artist, - 'releases': releases, - } - - def createIncludeTags(self): - return _createIncludes(self._includes) - - -class TrackIncludes(IIncludes): - """A specification on how much data to return with a track.""" - def __init__(self, artist=False, releases=False, puids=False, - artistRelations=False, releaseRelations=False, - trackRelations=False, urlRelations=False, tags=False, - ratings=False, isrcs=False): - self._includes = { - 'artist': artist, - 'releases': releases, - 'puids': puids, - 'artist-rels': artistRelations, - 'release-rels': releaseRelations, - 'track-rels': trackRelations, - 'url-rels': urlRelations, - 'tags': tags, - 'ratings': ratings, - 'isrcs': isrcs, - } - - def createIncludeTags(self): - return _createIncludes(self._includes) - - -class LabelIncludes(IIncludes): - """A specification on how much data to return with a label.""" - def __init__(self, aliases=False, tags=False, ratings=False): - self._includes = { - 'aliases': aliases, - 'tags': tags, - 'ratings': ratings, - } - - def createIncludeTags(self): - return _createIncludes(self._includes) - - -class Query(object): - """A simple interface to the MusicBrainz web service. - - This is a facade which provides a simple interface to the MusicBrainz - web service. It hides all the details like fetching data from a server, - parsing the XML and creating an object tree. Using this class, you can - request data by ID or search the I{collection} of all resources - (artists, releases, or tracks) to retrieve those matching given - criteria. This document contains examples to get you started. - - - Working with Identifiers - ======================== - - MusicBrainz uses absolute URIs as identifiers. For example, the artist - 'Tori Amos' is identified using the following URI:: - http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5 - - In some situations it is obvious from the context what type of - resource an ID refers to. In these cases, abbreviated identifiers may - be used, which are just the I{UUID} part of the URI. Thus the ID above - may also be written like this:: - c0b2500e-0cef-4130-869d-732b23ed9df5 - - All methods in this class which require IDs accept both the absolute - URI and the abbreviated form (aka the relative URI). - - - Creating a Query Object - ======================= - - In most cases, creating a L{Query} object is as simple as this: - - >>> import musicbrainz2.webservice as ws - >>> q = ws.Query() - >>> - - The instantiated object uses the standard L{WebService} class to - access the MusicBrainz web service. If you want to use a different - server or you have to pass user name and password because one of - your queries requires authentication, you have to create the - L{WebService} object yourself and configure it appropriately. - This example uses the MusicBrainz test server and also sets - authentication data: - - >>> import musicbrainz2.webservice as ws - >>> service = ws.WebService(host='test.musicbrainz.org', - ... username='whatever', password='secret') - >>> q = ws.Query(service) - >>> - - - Querying for Individual Resources - ================================= - - If the MusicBrainz ID of a resource is known, then the L{getArtistById}, - L{getReleaseById}, or L{getTrackById} method can be used to retrieve - it. Example: - - >>> import musicbrainz2.webservice as ws - >>> q = ws.Query() - >>> artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5') - >>> artist.name - u'Tori Amos' - >>> artist.sortName - u'Amos, Tori' - >>> print artist.type - http://musicbrainz.org/ns/mmd-1.0#Person - >>> - - This returned just the basic artist data, however. To get more detail - about a resource, the C{include} parameters may be used which expect - an L{ArtistIncludes}, L{ReleaseIncludes}, or L{TrackIncludes} object, - depending on the resource type. - - To get data about a release which also includes the main artist - and all tracks, for example, the following query can be used: - - >>> import musicbrainz2.webservice as ws - >>> q = ws.Query() - >>> releaseId = '33dbcf02-25b9-4a35-bdb7-729455f33ad7' - >>> include = ws.ReleaseIncludes(artist=True, tracks=True) - >>> release = q.getReleaseById(releaseId, include) - >>> release.title - u'Tales of a Librarian' - >>> release.artist.name - u'Tori Amos' - >>> release.tracks[0].title - u'Precious Things' - >>> - - Note that the query gets more expensive for the server the more - data you request, so please be nice. - - - Searching in Collections - ======================== - - For each resource type (artist, release, and track), there is one - collection which contains all resources of a type. You can search - these collections using the L{getArtists}, L{getReleases}, and - L{getTracks} methods. The collections are huge, so you have to - use filters (L{ArtistFilter}, L{ReleaseFilter}, or L{TrackFilter}) - to retrieve only resources matching given criteria. - - For example, If you want to search the release collection for - releases with a specified DiscID, you would use L{getReleases} - and a L{ReleaseFilter} object: - - >>> import musicbrainz2.webservice as ws - >>> q = ws.Query() - >>> filter = ws.ReleaseFilter(discId='8jJklE258v6GofIqDIrE.c5ejBE-') - >>> results = q.getReleases(filter=filter) - >>> results[0].score - 100 - >>> results[0].release.title - u'Under the Pink' - >>> - - The query returns a list of results (L{wsxml.ReleaseResult} objects - in this case), which are ordered by score, with a higher score - indicating a better match. Note that those results don't contain - all the data about a resource. If you need more detail, you can then - use the L{getArtistById}, L{getReleaseById}, or L{getTrackById} - methods to request the resource. - - All filters support the C{limit} argument to limit the number of - results returned. This defaults to 25, but the server won't send - more than 100 results to save bandwidth and processing power. Using - C{limit} and the C{offset} parameter, you can page through the - results. - - - Error Handling - ============== - - All methods in this class raise a L{WebServiceError} exception in case - of errors. Depending on the method, a subclass of L{WebServiceError} may - be raised which allows an application to handle errors more precisely. - The following example handles connection errors (invalid host name - etc.) separately and all other web service errors in a combined - catch clause: - - >>> try: - ... artist = q.getArtistById('c0b2500e-0cef-4130-869d-732b23ed9df5') - ... except ws.ConnectionError, e: - ... pass # implement your error handling here - ... except ws.WebServiceError, e: - ... pass # catches all other web service errors - ... - >>> - """ - - def __init__(self, ws=None, wsFactory=WebService, clientId=None): - """Constructor. - - The C{ws} parameter has to be a subclass of L{IWebService}. - If it isn't given, the C{wsFactory} parameter is used to - create an L{IWebService} subclass. - - If the constructor is called without arguments, an instance - of L{WebService} is used, preconfigured to use the MusicBrainz - server. This should be enough for most users. - - If you want to use queries which require authentication you - have to pass a L{WebService} instance where user name and - password have been set. - - The C{clientId} parameter is required for data submission. - The format is C{'application-version'}, where C{application} - is your application's name and C{version} is a version - number which may not include a '-' character. - - @param ws: a subclass instance of L{IWebService}, or None - @param wsFactory: a callable object which creates an object - @param clientId: a unicode string containing the application's ID - """ - if ws is None: - self._ws = wsFactory() - else: - self._ws = ws - - self._clientId = clientId - self._log = logging.getLogger(str(self.__class__)) - - - def getArtistById(self, id_, include=None): - """Returns an artist. - - If no artist with that ID can be found, C{include} contains - invalid tags or there's a server problem, an exception is - raised. - - @param id_: a string containing the artist's ID - @param include: an L{ArtistIncludes} object, or None - - @return: an L{Artist } object, or None - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResourceNotFoundError: artist doesn't exist - @raise ResponseError: server returned invalid data - """ - uuid = mbutils.extractUuid(id_, 'artist') - result = self._getFromWebService('artist', uuid, include) - artist = result.getArtist() - if artist is not None: - return artist - else: - raise ResponseError("server didn't return artist") - - - def getArtists(self, filter): - """Returns artists matching given criteria. - - @param filter: an L{ArtistFilter} object - - @return: a list of L{musicbrainz2.wsxml.ArtistResult} objects - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResponseError: server returned invalid data - """ - result = self._getFromWebService('artist', '', filter=filter) - return result.getArtistResults() - - def getLabelById(self, id_, include=None): - """Returns a L{model.Label} - - If no label with that ID can be found, or there is a server problem, - an exception is raised. - - @param id_: a string containing the label's ID. - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResourceNotFoundError: release doesn't exist - @raise ResponseError: server returned invalid data - """ - uuid = mbutils.extractUuid(id_, 'label') - result = self._getFromWebService('label', uuid, include) - label = result.getLabel() - if label is not None: - return label - else: - raise ResponseError("server didn't return a label") - - def getLabels(self, filter): - result = self._getFromWebService('label', '', filter=filter) - return result.getLabelResults() - - def getReleaseById(self, id_, include=None): - """Returns a release. - - If no release with that ID can be found, C{include} contains - invalid tags or there's a server problem, and exception is - raised. - - @param id_: a string containing the release's ID - @param include: a L{ReleaseIncludes} object, or None - - @return: a L{Release } object, or None - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResourceNotFoundError: release doesn't exist - @raise ResponseError: server returned invalid data - """ - uuid = mbutils.extractUuid(id_, 'release') - result = self._getFromWebService('release', uuid, include) - release = result.getRelease() - if release is not None: - return release - else: - raise ResponseError("server didn't return release") - - - def getReleases(self, filter): - """Returns releases matching given criteria. - - @param filter: a L{ReleaseFilter} object - - @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResponseError: server returned invalid data - """ - result = self._getFromWebService('release', '', filter=filter) - return result.getReleaseResults() - - def getReleaseGroupById(self, id_, include=None): - """Returns a release group. - - If no release group with that ID can be found, C{include} - contains invalid tags, or there's a server problem, an - exception is raised. - - @param id_: a string containing the release group's ID - @param include: a L{ReleaseGroupIncludes} object, or None - - @return: a L{ReleaseGroup } object, or None - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResourceNotFoundError: release doesn't exist - @raise ResponseError: server returned invalid data - """ - uuid = mbutils.extractUuid(id_, 'release-group') - result = self._getFromWebService('release-group', uuid, include) - releaseGroup = result.getReleaseGroup() - if releaseGroup is not None: - return releaseGroup - else: - raise ResponseError("server didn't return releaseGroup") - - def getReleaseGroups(self, filter): - """Returns release groups matching the given criteria. - - @param filter: a L{ReleaseGroupFilter} object - - @return: a list of L{musicbrainz2.wsxml.ReleaseGroupResult} objects - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResponseError: server returned invalid data - """ - result = self._getFromWebService('release-group', '', filter=filter) - return result.getReleaseGroupResults() - - def getTrackById(self, id_, include=None): - """Returns a track. - - If no track with that ID can be found, C{include} contains - invalid tags or there's a server problem, an exception is - raised. - - @param id_: a string containing the track's ID - @param include: a L{TrackIncludes} object, or None - - @return: a L{Track } object, or None - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResourceNotFoundError: track doesn't exist - @raise ResponseError: server returned invalid data - """ - uuid = mbutils.extractUuid(id_, 'track') - result = self._getFromWebService('track', uuid, include) - track = result.getTrack() - if track is not None: - return track - else: - raise ResponseError("server didn't return track") - - - def getTracks(self, filter): - """Returns tracks matching given criteria. - - @param filter: a L{TrackFilter} object - - @return: a list of L{musicbrainz2.wsxml.TrackResult} objects - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise ResponseError: server returned invalid data - """ - result = self._getFromWebService('track', '', filter=filter) - return result.getTrackResults() - - - def getUserByName(self, name): - """Returns information about a MusicBrainz user. - - You can only request user data if you know the user name and - password for that account. If username and/or password are - incorrect, an L{AuthenticationError} is raised. - - See the example in L{Query} on how to supply user name and - password. - - @param name: a unicode string containing the user's name - - @return: a L{User } object - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or include tags - @raise AuthenticationError: invalid user name and/or password - @raise ResourceNotFoundError: track doesn't exist - @raise ResponseError: server returned invalid data - """ - filter = UserFilter(name=name) - result = self._getFromWebService('user', '', None, filter) - - if len(result.getUserList()) > 0: - return result.getUserList()[0] - else: - raise ResponseError("response didn't contain user data") - - - def _getFromWebService(self, entity, id_, include=None, filter=None): - if filter is None: - filterParams = [ ] - else: - filterParams = filter.createParameters() - - if include is None: - includeParams = [ ] - else: - includeParams = include.createIncludeTags() - - stream = self._ws.get(entity, id_, includeParams, filterParams) - try: - parser = MbXmlParser() - return parser.parse(stream) - except ParseError, e: - raise ResponseError(str(e), e) - - - def submitPuids(self, tracks2puids): - """Submit track to PUID mappings. - - The C{tracks2puids} parameter has to be a dictionary, with the - keys being MusicBrainz track IDs (either as absolute URIs or - in their 36 character ASCII representation) and the values - being PUIDs (ASCII, 36 characters). - - Note that this method only works if a valid user name and - password have been set. See the example in L{Query} on how - to supply authentication data. - - @param tracks2puids: a dictionary mapping track IDs to PUIDs - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid track or PUIDs - @raise AuthenticationError: invalid user name and/or password - """ - assert self._clientId is not None, 'Please supply a client ID' - params = [ ] - params.append( ('client', self._clientId.encode('utf-8')) ) - - for (trackId, puid) in tracks2puids.iteritems(): - trackId = mbutils.extractUuid(trackId, 'track') - params.append( ('puid', trackId + ' ' + puid) ) - - encodedStr = urllib.urlencode(params, True) - - self._ws.post('track', '', encodedStr) - - def submitISRCs(self, tracks2isrcs): - """Submit track to ISRC mappings. - - The C{tracks2isrcs} parameter has to be a dictionary, with the - keys being MusicBrainz track IDs (either as absolute URIs or - in their 36 character ASCII representation) and the values - being ISRCs (ASCII, 12 characters). - - Note that this method only works if a valid user name and - password have been set. See the example in L{Query} on how - to supply authentication data. - - @param tracks2isrcs: a dictionary mapping track IDs to ISRCs - - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid track or ISRCs - @raise AuthenticationError: invalid user name and/or password - """ - params = [ ] - - for (trackId, isrc) in tracks2isrcs.iteritems(): - trackId = mbutils.extractUuid(trackId, 'track') - params.append( ('isrc', trackId + ' ' + isrc) ) - - encodedStr = urllib.urlencode(params, True) - - self._ws.post('track', '', encodedStr) - - def addToUserCollection(self, releases): - """Add releases to a user's collection. - - The releases parameter must be a list. It can contain either L{Release} - objects or a string representing a MusicBrainz release ID (either as - absolute URIs or in their 36 character ASCII representation). - - Adding a release that is already in the collection has no effect. - - @param releases: a list of releases to add to the user collection - - @raise ConnectionError: couldn't connect to server - @raise AuthenticationError: invalid user name and/or password - """ - rels = [] - for release in releases: - if isinstance(release, Release): - rels.append(mbutils.extractUuid(release.id)) - else: - rels.append(mbutils.extractUuid(release)) - encodedStr = urllib.urlencode({'add': ",".join(rels)}, True) - self._ws.post('collection', '', encodedStr) - - def removeFromUserCollection(self, releases): - """Remove releases from a user's collection. - - The releases parameter must be a list. It can contain either L{Release} - objects or a string representing a MusicBrainz release ID (either as - absolute URIs or in their 36 character ASCII representation). - - Removing a release that is not in the collection has no effect. - - @param releases: a list of releases to remove from the user collection - - @raise ConnectionError: couldn't connect to server - @raise AuthenticationError: invalid user name and/or password - """ - rels = [] - for release in releases: - if isinstance(release, Release): - rels.append(mbutils.extractUuid(release.id)) - else: - rels.append(mbutils.extractUuid(release)) - encodedStr = urllib.urlencode({'remove': ",".join(rels)}, True) - self._ws.post('collection', '', encodedStr) - - def getUserCollection(self, offset=0, maxitems=100): - """Get the releases that are in a user's collection - - A maximum of 100 items will be returned for any one call - to this method. To fetch more than 100 items, use the offset - parameter. - - @param offset: the offset to start fetching results from - @param maxitems: the upper limit on items to return - - @return: a list of L{musicbrainz2.wsxml.ReleaseResult} objects - - @raise ConnectionError: couldn't connect to server - @raise AuthenticationError: invalid user name and/or password - """ - params = { 'offset': offset, 'maxitems': maxitems } - - stream = self._ws.get('collection', '', filter=params) - print stream - try: - parser = MbXmlParser() - result = parser.parse(stream) - except ParseError, e: - raise ResponseError(str(e), e) - - return result.getReleaseResults() - - def submitUserTags(self, entityUri, tags): - """Submit folksonomy tags for an entity. - - Note that all previously existing tags from the authenticated - user are replaced with the ones given to this method. Other - users' tags are not affected. - - @param entityUri: a string containing an absolute MB ID - @param tags: A list of either L{Tag } objects - or strings - - @raise ValueError: invalid entityUri - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID, entity or tags - @raise AuthenticationError: invalid user name and/or password - """ - entity = mbutils.extractEntityType(entityUri) - uuid = mbutils.extractUuid(entityUri, entity) - params = ( - ('type', 'xml'), - ('entity', entity), - ('id', uuid), - ('tags', ','.join([unicode(tag).encode('utf-8') for tag in tags])) - ) - - encodedStr = urllib.urlencode(params) - - self._ws.post('tag', '', encodedStr) - - - def getUserTags(self, entityUri): - """Returns a list of folksonomy tags a user has applied to an entity. - - The given parameter has to be a fully qualified MusicBrainz ID, as - returned by other library functions. - - Note that this method only works if a valid user name and - password have been set. Only the tags the authenticated user - applied to the entity will be returned. If username and/or - password are incorrect, an AuthenticationError is raised. - - This method will return a list of L{Tag } - objects. - - @param entityUri: a string containing an absolute MB ID - - @raise ValueError: invalid entityUri - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or entity - @raise AuthenticationError: invalid user name and/or password - """ - entity = mbutils.extractEntityType(entityUri) - uuid = mbutils.extractUuid(entityUri, entity) - params = { 'entity': entity, 'id': uuid } - - stream = self._ws.get('tag', '', filter=params) - try: - parser = MbXmlParser() - result = parser.parse(stream) - except ParseError, e: - raise ResponseError(str(e), e) - - return result.getTagList() - - def submitUserRating(self, entityUri, rating): - """Submit rating for an entity. - - Note that all previously existing rating from the authenticated - user are replaced with the one given to this method. Other - users' ratings are not affected. - - @param entityUri: a string containing an absolute MB ID - @param rating: A L{Rating } object - or integer - - @raise ValueError: invalid entityUri - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID, entity or tags - @raise AuthenticationError: invalid user name and/or password - """ - entity = mbutils.extractEntityType(entityUri) - uuid = mbutils.extractUuid(entityUri, entity) - params = ( - ('type', 'xml'), - ('entity', entity), - ('id', uuid), - ('rating', unicode(rating).encode('utf-8')) - ) - - encodedStr = urllib.urlencode(params) - - self._ws.post('rating', '', encodedStr) - - - def getUserRating(self, entityUri): - """Return the rating a user has applied to an entity. - - The given parameter has to be a fully qualified MusicBrainz - ID, as returned by other library functions. - - Note that this method only works if a valid user name and - password have been set. Only the rating the authenticated user - applied to the entity will be returned. If username and/or - password are incorrect, an AuthenticationError is raised. - - This method will return a L{Rating } - object. - - @param entityUri: a string containing an absolute MB ID - - @raise ValueError: invalid entityUri - @raise ConnectionError: couldn't connect to server - @raise RequestError: invalid ID or entity - @raise AuthenticationError: invalid user name and/or password - """ - entity = mbutils.extractEntityType(entityUri) - uuid = mbutils.extractUuid(entityUri, entity) - params = { 'entity': entity, 'id': uuid } - - stream = self._ws.get('rating', '', filter=params) - try: - parser = MbXmlParser() - result = parser.parse(stream) - except ParseError, e: - raise ResponseError(str(e), e) - - return result.getRating() - - def submitCDStub(self, cdstub): - """Submit a CD Stub to the database. - - The number of tracks added to the CD Stub must match the TOC and DiscID - otherwise the submission wil fail. The submission will also fail if - the Disc ID is already in the MusicBrainz database. - - This method will only work if no user name and password are set. - - @param cdstub: a L{CDStub} object to submit - - @raise RequestError: Missmatching TOC/Track information or the - the CD Stub already exists or the Disc ID already exists - """ - assert self._clientId is not None, 'Please supply a client ID' - disc = cdstub._disc - params = [ ] - params.append( ('client', self._clientId.encode('utf-8')) ) - params.append( ('discid', disc.id) ) - params.append( ('title', cdstub.title) ) - params.append( ('artist', cdstub.artist) ) - if cdstub.barcode != "": - params.append( ('barcode', cdstub.barcode) ) - if cdstub.comment != "": - params.append( ('comment', cdstub.comment) ) - - trackind = 0 - for track,artist in cdstub.tracks: - params.append( ('track%d' % trackind, track) ) - if artist != "": - params.append( ('artist%d' % trackind, artist) ) - - trackind += 1 - - toc = "%d %d %d " % (disc.firstTrackNum, disc.lastTrackNum, disc.sectors) - toc = toc + ' '.join( map(lambda x: str(x[0]), disc.getTracks()) ) - - params.append( ('toc', toc) ) - - encodedStr = urllib.urlencode(params) - self._ws.post('release', '', encodedStr) - -def _createIncludes(tagMap): - selected = filter(lambda x: x[1] == True, tagMap.items()) - return map(lambda x: x[0], selected) - -def _createParameters(params): - """Remove (x, None) tuples and encode (x, str/unicode) to utf-8.""" - ret = [ ] - for p in params: - if isinstance(p[1], (str, unicode)): - ret.append( (p[0], p[1].encode('utf-8')) ) - elif p[1] is not None: - ret.append(p) - - return ret - -def _paramsValid(params): - """Check if the query parameter collides with other parameters.""" - tmp = [ ] - for name, value in params: - if value is not None and name not in ('offset', 'limit'): - tmp.append(name) - - if 'query' in tmp and len(tmp) > 1: - return False - else: - return True - -if __name__ == '__main__': - import doctest - doctest.testmod() - -# EOF diff --git a/lib/musicbrainz2/wsxml.py b/lib/musicbrainz2/wsxml.py deleted file mode 100644 index 7d031ca5..00000000 --- a/lib/musicbrainz2/wsxml.py +++ /dev/null @@ -1,1675 +0,0 @@ -"""A parser for the Music Metadata XML Format (MMD). - -This module contains L{MbXmlParser}, which parses the U{Music Metadata XML -Format (MMD) } returned by the -MusicBrainz webservice. - -There are also DOM helper functions in this module used by the parser which -probably aren't useful to users. -""" -__revision__ = '$Id: wsxml.py 12028 2009-09-01 13:15:50Z matt $' - -import re -import logging -import urlparse -import xml.dom.minidom -import xml.sax.saxutils as saxutils -from xml.parsers.expat import ExpatError -from xml.dom import DOMException - -import lib.musicbrainz2.utils as mbutils -import lib.musicbrainz2.model as model -from lib.musicbrainz2.model import NS_MMD_1, NS_REL_1, NS_EXT_1 - -__all__ = [ - 'DefaultFactory', 'Metadata', 'ParseError', - 'MbXmlParser', 'MbXmlWriter', - 'AbstractResult', - 'ArtistResult', 'ReleaseResult', 'TrackResult', 'LabelResult', - 'ReleaseGroupResult' -] - - -class DefaultFactory(object): - """A factory to instantiate classes from the domain model. - - This factory may be used to create objects from L{musicbrainz2.model}. - """ - def newArtist(self): return model.Artist() - def newRelease(self): return model.Release() - def newReleaseGroup(self): return model.ReleaseGroup() - def newTrack(self): return model.Track() - def newRelation(self): return model.Relation() - def newReleaseEvent(self): return model.ReleaseEvent() - def newDisc(self): return model.Disc() - def newArtistAlias(self): return model.ArtistAlias() - def newUser(self): return model.User() - def newLabel(self): return model.Label() - def newLabelAlias(self): return model.LabelAlias() - def newTag(self): return model.Tag() - def newRating(self): return model.Rating() - - -class ParseError(Exception): - """Exception to be thrown if a parse error occurs. - - The C{'msg'} attribute contains a printable error message, C{'reason'} - is the lower level exception that was raised. - """ - - def __init__(self, msg='Parse Error', reason=None): - Exception.__init__(self) - self.msg = msg - self.reason = reason - - def __str__(self): - return self.msg - - -class Metadata(object): - """Represents a parsed Music Metadata XML document. - - The Music Metadata XML format is very flexible and may contain a - diverse set of data (e.g. an artist, a release and a list of tracks), - but usually only a small subset is used (either an artist, a release - or a track, or a lists of objects from one class). - - @see: L{MbXmlParser} for reading, and L{MbXmlWriter} for writing - Metadata objects - """ - def __init__(self): - self._artist = None - self._release = None - self._track = None - self._label = None - self._releaseGroup = None - self._artistResults = [ ] - self._artistResultsOffset = None - self._artistResultsCount = None - self._releaseResults = [ ] - self._releaseResultsOffset = None - self._releaseResultsCount = None - self._releaseGroupResults = [ ] - self._releaseGroupResultsOffset = None - self._releaseGroupResultsCount = None - self._trackResults = [ ] - self._trackResultsOffset = None - self._trackResultsCount = None - self._labelResults = [ ] - self._labelResultsOffset = None - self._labelResultsCount = None - self._tagList = [ ] - self._rating = None - self._userList = [ ] - - def getArtist(self): - return self._artist - - def setArtist(self, artist): - self._artist = artist - - artist = property(getArtist, setArtist, doc='An Artist object.') - - def getLabel(self): - return self._label - - def setLabel(self, label): - self._label = label - - label = property(getLabel, setLabel, doc='A Label object.') - - def getRelease(self): - return self._release - - def setRelease(self, release): - self._release = release - - release = property(getRelease, setRelease, doc='A Release object.') - - def getReleaseGroup(self): - return self._releaseGroup - - def setReleaseGroup(self, releaseGroup): - self._releaseGroup = releaseGroup - - releaseGroup = property(getReleaseGroup, setReleaseGroup) - - def getTrack(self): - return self._track - - def setTrack(self, track): - self._track = track - - track = property(getTrack, setTrack, doc='A Track object.') - - def getArtistResults(self): - """Returns an artist result list. - - @return: a list of L{ArtistResult} objects. - """ - return self._artistResults - - artistResults = property(getArtistResults, - doc='A list of ArtistResult objects.') - - def getArtistResultsOffset(self): - """Returns the offset of the artist result list. - - The offset is used for paging through the result list. It - is zero-based. - - @return: an integer containing the offset, or None - - @see: L{getArtistResults}, L{getArtistResultsCount} - """ - return self._artistResultsOffset - - def setArtistResultsOffset(self, value): - """Sets the offset of the artist result list. - - @param value: an integer containing the offset, or None - - @see: L{getArtistResultsOffset} - """ - self._artistResultsOffset = value - - artistResultsOffset = property( - getArtistResultsOffset, setArtistResultsOffset, - doc='The offset of the artist results.') - - def getArtistResultsCount(self): - """Returns the total number of results available. - - This may or may not match with the number of elements that - L{getArtistResults} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setArtistResultsCount}, L{getArtistResultsOffset} - """ - return self._artistResultsCount - - def setArtistResultsCount(self, value): - """Sets the total number of available results. - - @param value: an integer containing the count, or None - - @see: L{getArtistResults}, L{setArtistResultsOffset} - """ - self._artistResultsCount = value - - artistResultsCount = property( - getArtistResultsCount, setArtistResultsCount, - doc='The total number of artists results.') - - def getLabelResults(self): - """Returns a label result list. - - @return: a list of L{LabelResult} objects. - """ - return self._labelResults - - labelResults = property(getLabelResults, - doc='A list of LabelResult objects') - - def getLabelResultsOffset(self): - """Returns the offset of the label result list. - - The offset is used for paging through the result list. It - is zero-based. - - @return: an integer containing the offset, or None - - @see: L{getLabelResults}, L{getLabelResultsCount} - """ - return self._labelResultsOffset - - def setLabelResultsOffset(self, value): - """Sets the offset of the label result list. - - @param value: an integer containing the offset, or None - - @see: L{getLabelResultsOffset} - """ - self._labelResultsOffset = value - - labelResultsOffset = property( - getLabelResultsOffset, setLabelResultsOffset, - doc='The offset of the label results.') - - def getLabelResultsCount(self): - """Returns the total number of results available. - - This may or may not match with the number of elements that - L{getLabelResults} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setLabelResultsCount}, L{getLabelResultsOffset} - """ - return self._labelResultsCount - - def setLabelResultsCount(self, value): - """Sets the total number of available results. - - @param value: an integer containing the count, or None - - @see: L{getLabelResults}, L{setLabelResultsOffset} - """ - self._labelResultsCount = value - - labelResultsCount = property( - getLabelResultsCount, setLabelResultsCount, - doc='The total number of label results.') - - def getReleaseResults(self): - """Returns a release result list. - - @return: a list of L{ReleaseResult} objects. - """ - return self._releaseResults - - releaseResults = property(getReleaseResults, - doc='A list of ReleaseResult objects.') - - def getReleaseResultsOffset(self): - """Returns the offset of the release result list. - - The offset is used for paging through the result list. It - is zero-based. - - @return: an integer containing the offset, or None - - @see: L{getReleaseResults}, L{getReleaseResultsCount} - """ - return self._releaseResultsOffset - - def setReleaseResultsOffset(self, value): - """Sets the offset of the release result list. - - @param value: an integer containing the offset, or None - - @see: L{getReleaseResultsOffset} - """ - self._releaseResultsOffset = value - - releaseResultsOffset = property( - getReleaseResultsOffset, setReleaseResultsOffset, - doc='The offset of the release results.') - - def getReleaseResultsCount(self): - """Returns the total number of results available. - - This may or may not match with the number of elements that - L{getReleaseResults} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setReleaseResultsCount}, L{getReleaseResultsOffset} - """ - return self._releaseResultsCount - - def setReleaseResultsCount(self, value): - """Sets the total number of available results. - - @param value: an integer containing the count, or None - - @see: L{getReleaseResults}, L{setReleaseResultsOffset} - """ - self._releaseResultsCount = value - - releaseResultsCount = property( - getReleaseResultsCount, setReleaseResultsCount, - doc='The total number of release results.') - - def getReleaseGroupResults(self): - """Returns a release group result list. - - @return: a list of L{ReleaseGroupResult} objects. - """ - return self._releaseGroupResults - - releaseGroupResults = property(getReleaseGroupResults, - doc = 'A list of ReleaseGroupResult objects.') - - def getReleaseGroupResultsOffset(self): - """Returns the offset of the release group result list. - - The offset is used for paging through the result list. It - is zero-based. - - @return: an integer containing the offset, or None. - - @see: L{getReleaseGroupResults}, L{getReleaseGroupResultsCount} - """ - return self._releaseGroupResultsOffset - - def setReleaseGroupResultsOffset(self, value): - """Sets the offset of the release group result list. - - @param value: an integer containing the offset, or None - - @see: L{getReleaseGroupResultsOffset} - """ - self._releaseGroupResultsOffset = value - - releaseGroupResultsOffset = property( - getReleaseGroupResultsOffset, setReleaseGroupResultsOffset, - doc='The offset of the release group results.') - - def getReleaseGroupResultsCount(self): - """Returns the total number of results available. - - This may or may not match with the number of elements that - L{getReleaseGroupResults} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setReleaseGroupResultsCount}, L{getReleaseGroupResultsOffset} - """ - return self._releaseGroupResultsCount - - def setReleaseGroupResultsCount(self, value): - """Sets the total number of available results. - - @param value: an integer containing the count, or None - - @see: L{getReleaseGroupResults}, L{setReleaseGroupResultsOffset} - """ - self._releaseGroupResultsCount = value - - releaseGroupResultsCount = property( - getReleaseGroupResultsCount, setReleaseGroupResultsCount, - doc='The total number of release group results.') - - def getTrackResults(self): - """Returns a track result list. - - @return: a list of L{TrackResult} objects. - """ - return self._trackResults - - trackResults = property(getTrackResults, - doc='A list of TrackResult objects.') - - def getTrackResultsOffset(self): - """Returns the offset of the track result list. - - The offset is used for paging through the result list. It - is zero-based. - - @return: an integer containing the offset, or None - - @see: L{getTrackResults}, L{getTrackResultsCount} - """ - return self._trackResultsOffset - - def setTrackResultsOffset(self, value): - """Sets the offset of the track result list. - - @param value: an integer containing the offset, or None - - @see: L{getTrackResultsOffset} - """ - self._trackResultsOffset = value - - trackResultsOffset = property( - getTrackResultsOffset, setTrackResultsOffset, - doc='The offset of the track results.') - - def getTrackResultsCount(self): - """Returns the total number of results available. - - This may or may not match with the number of elements that - L{getTrackResults} returns. If the count is higher than - the list, it indicates that the list is incomplete. - - @return: an integer containing the count, or None - - @see: L{setTrackResultsCount}, L{getTrackResultsOffset} - """ - return self._trackResultsCount - - def setTrackResultsCount(self, value): - """Sets the total number of available results. - - @param value: an integer containing the count, or None - - @see: L{getTrackResults}, L{setTrackResultsOffset} - """ - self._trackResultsCount = value - - trackResultsCount = property( - getTrackResultsCount, setTrackResultsCount, - doc='The total number of track results.') - - - def getTagList(self): - """Returns a list of tags. - - @return: a list of L{model.Tag} objects - """ - return self._tagList - - tagResults = property(getTagList, - doc='A list of Tag objects.') - - def getRating(self): - """Returns the rating. - - @return: rating object - """ - return self._rating - - def setRating(self, value): - """Sets the rating. - - @param value: a L{model.Rating} object - """ - self._rating = value - - rating = property(getRating, setRating, doc='A Rating object.') - - - # MusicBrainz extension to the schema - def getUserList(self): - """Returns a list of users. - - @return: a list of L{model.User} objects - - @note: This is a MusicBrainz extension. - """ - return self._userList - - userResults = property(getUserList, - doc='A list of User objects.') - - -class AbstractResult(object): - """The abstract representation of a result. - - A result is an instance of some kind (Artist, Release, ...) - associated with a score. - """ - - def __init__(self, score): - self._score = score - - def getScore(self): - """Returns the result score. - - The score indicates how good this result matches the search - parameters. The higher the value, the better the match. - - @return: an int between 0 and 100 (both inclusive), or None - """ - return self._score - - def setScore(self, score): - self._score = score - - score = property(getScore, setScore, doc='The relevance score.') - - -class ArtistResult(AbstractResult): - """Represents an artist result. - - An ArtistResult consists of a I{score} and an artist. The score is a - number between 0 and 100, where a higher number indicates a better - match. - """ - def __init__(self, artist, score): - super(ArtistResult, self).__init__(score) - self._artist = artist - - def getArtist(self): - """Returns an Artist object. - - @return: a L{musicbrainz2.model.Artist} object - """ - return self._artist - - def setArtist(self, artist): - self._artist = artist - - artist = property(getArtist, setArtist, doc='An Artist object.') - - -class ReleaseResult(AbstractResult): - """Represents a release result. - - A ReleaseResult consists of a I{score} and a release. The score is a - number between 0 and 100, where a higher number indicates a better - match. - """ - def __init__(self, release, score): - super(ReleaseResult, self).__init__(score) - self._release = release - - def getRelease(self): - """Returns a Release object. - - @return: a L{musicbrainz2.model.Release} object - """ - return self._release - - def setRelease(self, release): - self._release = release - - release = property(getRelease, setRelease, doc='A Release object.') - -class ReleaseGroupResult(AbstractResult): - """Represents a release group result. - - A ReleaseGroupResult consists of a I{score} and a release group. The - score is a number between 0 and 100, where a higher number indicates - a better match. - """ - def __init__(self, releaseGroup, score): - super(ReleaseGroupResult, self).__init__(score) - self._releaseGroup = releaseGroup - - def getReleaseGroup(self): - """Returns a ReleaseGroup object. - - @return: a L{musicbrainz2.model.ReleaseGroup} object - """ - return self._releaseGroup - - def setReleaseGroup(self, value): - self._releaseGroup = value - - releaseGroup = property(getReleaseGroup, setReleaseGroup, doc='A ReleaseGroup object.') - -class TrackResult(AbstractResult): - """Represents a track result. - - A TrackResult consists of a I{score} and a track. The score is a - number between 0 and 100, where a higher number indicates a better - match. - """ - def __init__(self, track, score): - super(TrackResult, self).__init__(score) - self._track = track - - def getTrack(self): - """Returns a Track object. - - @return: a L{musicbrainz2.model.Track} object - """ - return self._track - - def setTrack(self, track): - self._track = track - - track = property(getTrack, setTrack, doc='A Track object.') - - -class LabelResult(AbstractResult): - """Represents a label result. - - An LabelResult consists of a I{score} and a label. The score is a - number between 0 and 100, where a higher number indicates a better - match. - """ - def __init__(self, label, score): - super(LabelResult, self).__init__(score) - self._label = label - - def getLabel(self): - """Returns a Label object. - - @return: a L{musicbrainz2.model.Label} object - """ - return self._label - - def setLabel(self, label): - self._label = label - - label = property(getLabel, setLabel, doc='A Label object.') - - -class MbXmlParser(object): - """A parser for the Music Metadata XML format. - - This parser supports all basic features and extensions defined by - MusicBrainz, including unlimited document nesting. By default it - reads an XML document from a file-like object (stream) and returns - an object tree representing the document using classes from - L{musicbrainz2.model}. - - The implementation tries to be as permissive as possible. Invalid - contents are skipped, but documents have to be well-formed and using - the correct namespace. In case of unrecoverable errors, a L{ParseError} - exception is raised. - - @see: U{The Music Metadata XML Format - } - """ - - def __init__(self, factory=DefaultFactory()): - """Constructor. - - The C{factory} parameter has be an instance of L{DefaultFactory} - or a subclass of it. It is used by L{parse} to obtain objects - from L{musicbrainz2.model} to build resulting object tree. - If you supply your own factory, you have to make sure all - returned objects have the same interface as their counterparts - from L{musicbrainz2.model}. - - @param factory: an object factory - """ - self._log = logging.getLogger(str(self.__class__)) - self._factory = factory - - def parse(self, inStream): - """Parses the MusicBrainz web service XML. - - Returns a L{Metadata} object representing the parsed XML or - raises a L{ParseError} exception if the data was malformed. - The parser tries to be liberal and skips invalid content if - possible. - - Note that an L{IOError} may be raised if there is a problem - reading C{inStream}. - - @param inStream: a file-like object - @return: a L{Metadata} object (never None) - @raise ParseError: if the document is not valid - @raise IOError: if reading from the stream failed - """ - - try: - doc = xml.dom.minidom.parse(inStream) - - # Try to find the root element. If this isn't an mmd - # XML file or the namespace is wrong, this will fail. - elems = doc.getElementsByTagNameNS(NS_MMD_1, 'metadata') - - if len(elems) != 0: - md = self._createMetadata(elems[0]) - else: - msg = 'cannot find root element mmd:metadata' - self._log.debug('ParseError: ' + msg) - raise ParseError(msg) - - doc.unlink() - - return md - except ExpatError, e: - self._log.debug('ExpatError: ' + str(e)) - raise ParseError(msg=str(e), reason=e) - except DOMException, e: - self._log.debug('DOMException: ' + str(e)) - raise ParseError(msg=str(e), reason=e) - - - def _createMetadata(self, metadata): - md = Metadata() - - for node in _getChildElements(metadata): - if _matches(node, 'artist'): - md.artist = self._createArtist(node) - elif _matches(node, 'release'): - md.release = self._createRelease(node) - elif _matches(node, 'release-group'): - md.releaseGroup = self._createReleaseGroup(node) - elif _matches(node, 'track'): - md.track = self._createTrack(node) - elif _matches(node, 'label'): - md.label = self._createLabel(node) - elif _matches(node, 'artist-list'): - (offset, count) = self._getListAttrs(node) - md.artistResultsOffset = offset - md.artistResultsCount = count - self._addArtistResults(node, md.getArtistResults()) - elif _matches(node, 'release-list'): - (offset, count) = self._getListAttrs(node) - md.releaseResultsOffset = offset - md.releaseResultsCount = count - self._addReleaseResults(node, md.getReleaseResults()) - elif _matches(node, 'release-group-list'): - (offset, count) = self._getListAttrs(node) - md.releaseGroupResultsOffset = offset - md.releaseGroupResultsCount = count - self._addReleaseGroupResults(node, md.getReleaseGroupResults()) - elif _matches(node, 'track-list'): - (offset, count) = self._getListAttrs(node) - md.trackResultsOffset = offset - md.trackResultsCount = count - self._addTrackResults(node, md.getTrackResults()) - elif _matches(node, 'label-list'): - (offset, count) = self._getListAttrs(node) - md.labelResultsOffset = offset - md.labelResultsCount = count - self._addLabelResults(node, md.getLabelResults()) - elif _matches(node, 'tag-list'): - self._addTagsToList(node, md.getTagList()) - elif _matches(node, 'user-list', NS_EXT_1): - self._addUsersToList(node, md.getUserList()) - - return md - - - def _addArtistResults(self, listNode, resultList): - for c in _getChildElements(listNode): - artist = self._createArtist(c) - score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1) - if artist is not None: - resultList.append(ArtistResult(artist, score)) - - def _addReleaseResults(self, listNode, resultList): - for c in _getChildElements(listNode): - release = self._createRelease(c) - score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1) - if release is not None: - resultList.append(ReleaseResult(release, score)) - - def _addReleaseGroupResults(self, listNode, resultList): - for c in _getChildElements(listNode): - releaseGroup = self._createReleaseGroup(c) - score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1) - if releaseGroup is not None: - resultList.append(ReleaseGroupResult(releaseGroup, score)) - - def _addTrackResults(self, listNode, resultList): - for c in _getChildElements(listNode): - track = self._createTrack(c) - score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1) - if track is not None: - resultList.append(TrackResult(track, score)) - - def _addLabelResults(self, listNode, resultList): - for c in _getChildElements(listNode): - label = self._createLabel(c) - score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1) - if label is not None: - resultList.append(LabelResult(label, score)) - - def _addReleasesToList(self, listNode, resultList): - self._addToList(listNode, resultList, self._createRelease) - - def _addReleaseGroupsToList(self, listNode, resultList): - self._addToList(listNode, resultList, self._createReleaseGroup) - - def _addTracksToList(self, listNode, resultList): - self._addToList(listNode, resultList, self._createTrack) - - def _addUsersToList(self, listNode, resultList): - self._addToList(listNode, resultList, self._createUser) - - def _addTagsToList(self, listNode, resultList): - self._addToList(listNode, resultList, self._createTag) - - def _addTagsToEntity(self, listNode, entity): - for node in _getChildElements(listNode): - tag = self._createTag(node) - entity.addTag(tag) - - def _addRatingToEntity(self, attrNode, entity): - rating = self._createRating(attrNode) - entity.setRating(rating) - - def _addToList(self, listNode, resultList, creator): - for c in _getChildElements(listNode): - resultList.append(creator(c)) - - def _getListAttrs(self, listNode): - offset = _getIntAttr(listNode, 'offset') - count = _getIntAttr(listNode, 'count') - return (offset, count) - - - def _createArtist(self, artistNode): - artist = self._factory.newArtist() - artist.setId(_getIdAttr(artistNode, 'id', 'artist')) - artist.setType(_getUriAttr(artistNode, 'type')) - - for node in _getChildElements(artistNode): - if _matches(node, 'name'): - artist.setName(_getText(node)) - elif _matches(node, 'sort-name'): - artist.setSortName(_getText(node)) - elif _matches(node, 'disambiguation'): - artist.setDisambiguation(_getText(node)) - elif _matches(node, 'life-span'): - artist.setBeginDate(_getDateAttr(node, 'begin')) - artist.setEndDate(_getDateAttr(node, 'end')) - elif _matches(node, 'alias-list'): - self._addArtistAliases(node, artist) - elif _matches(node, 'release-list'): - (offset, count) = self._getListAttrs(node) - artist.setReleasesOffset(offset) - artist.setReleasesCount(count) - self._addReleasesToList(node, artist.getReleases()) - elif _matches(node, 'release-group-list'): - (offset, count) = self._getListAttrs(node) - artist.setReleaseGroupsOffset(offset) - artist.setReleaseGroupsCount(count) - self._addReleaseGroupsToList(node, artist.getReleaseGroups()) - elif _matches(node, 'relation-list'): - self._addRelationsToEntity(node, artist) - elif _matches(node, 'tag-list'): - self._addTagsToEntity(node, artist) - elif _matches(node, 'rating'): - self._addRatingToEntity(node, artist) - - return artist - - def _createLabel(self, labelNode): - label = self._factory.newLabel() - label.setId(_getIdAttr(labelNode, 'id', 'label')) - label.setType(_getUriAttr(labelNode, 'type')) - - for node in _getChildElements(labelNode): - if _matches(node, 'name'): - label.setName(_getText(node)) - if _matches(node, 'sort-name'): - label.setSortName(_getText(node)) - elif _matches(node, 'disambiguation'): - label.setDisambiguation(_getText(node)) - elif _matches(node, 'label-code'): - label.setCode(_getText(node)) - elif _matches(node, 'country'): - country = _getText(node, '^[A-Z]{2}$') - label.setCountry(country) - elif _matches(node, 'life-span'): - label.setBeginDate(_getDateAttr(node, 'begin')) - label.setEndDate(_getDateAttr(node, 'end')) - elif _matches(node, 'alias-list'): - self._addLabelAliases(node, label) - elif _matches(node, 'tag-list'): - self._addTagsToEntity(node, label) - elif _matches(node, 'rating'): - self._addRatingToEntity(node, label) - - return label - - def _createRelease(self, releaseNode): - release = self._factory.newRelease() - release.setId(_getIdAttr(releaseNode, 'id', 'release')) - for t in _getUriListAttr(releaseNode, 'type'): - release.addType(t) - - for node in _getChildElements(releaseNode): - if _matches(node, 'title'): - release.setTitle(_getText(node)) - elif _matches(node, 'text-representation'): - lang = _getAttr(node, 'language', '^[A-Z]{3}$') - release.setTextLanguage(lang) - script = _getAttr(node, 'script', '^[A-Z][a-z]{3}$') - release.setTextScript(script) - elif _matches(node, 'asin'): - release.setAsin(_getText(node)) - elif _matches(node, 'artist'): - release.setArtist(self._createArtist(node)) - elif _matches(node, 'release-event-list'): - self._addReleaseEvents(node, release) - elif _matches(node, 'release-group'): - release.setReleaseGroup(self._createReleaseGroup(node)) - elif _matches(node, 'disc-list'): - self._addDiscs(node, release) - elif _matches(node, 'track-list'): - (offset, count) = self._getListAttrs(node) - release.setTracksOffset(offset) - release.setTracksCount(count) - self._addTracksToList(node, release.getTracks()) - elif _matches(node, 'relation-list'): - self._addRelationsToEntity(node, release) - elif _matches(node, 'tag-list'): - self._addTagsToEntity(node, release) - elif _matches(node, 'rating'): - self._addRatingToEntity(node, release) - - return release - - def _createReleaseGroup(self, node): - rg = self._factory.newReleaseGroup() - rg.setId(_getIdAttr(node, 'id', 'release-group')) - rg.setType(_getUriAttr(node, 'type')) - - for child in _getChildElements(node): - if _matches(child, 'title'): - rg.setTitle(_getText(child)) - elif _matches(child, 'artist'): - rg.setArtist(self._createArtist(child)) - elif _matches(child, 'release-list'): - (offset, count) = self._getListAttrs(child) - rg.setReleasesOffset(offset) - rg.setReleasesCount(count) - self._addReleasesToList(child, rg.getReleases()) - - return rg - - def _addReleaseEvents(self, releaseListNode, release): - for node in _getChildElements(releaseListNode): - if _matches(node, 'event'): - country = _getAttr(node, 'country', '^[A-Z]{2}$') - date = _getDateAttr(node, 'date') - catalogNumber = _getAttr(node, 'catalog-number') - barcode = _getAttr(node, 'barcode') - format = _getUriAttr(node, 'format') - - # The date attribute is mandatory. If it isn't present, - # we don't add anything from this release event. - if date is not None: - event = self._factory.newReleaseEvent() - event.setCountry(country) - event.setDate(date) - event.setCatalogNumber(catalogNumber) - event.setBarcode(barcode) - event.setFormat(format) - - for subNode in _getChildElements(node): - if _matches(subNode, 'label'): - event.setLabel(self._createLabel(subNode)) - - release.addReleaseEvent(event) - - - def _addDiscs(self, discIdListNode, release): - for node in _getChildElements(discIdListNode): - if _matches(node, 'disc') and node.hasAttribute('id'): - d = self._factory.newDisc() - d.setId(node.getAttribute('id')) - d.setSectors(_getIntAttr(node, 'sectors', 0)) - release.addDisc(d) - - - def _addArtistAliases(self, aliasListNode, artist): - for node in _getChildElements(aliasListNode): - if _matches(node, 'alias'): - alias = self._factory.newArtistAlias() - self._initializeAlias(alias, node) - artist.addAlias(alias) - - - def _addLabelAliases(self, aliasListNode, label): - for node in _getChildElements(aliasListNode): - if _matches(node, 'alias'): - alias = self._factory.newLabelAlias() - self._initializeAlias(alias, node) - label.addAlias(alias) - - - def _initializeAlias(self, alias, node): - alias.setValue(_getText(node)) - alias.setType(_getUriAttr(node, 'type')) - alias.setScript(_getAttr(node, 'script', - '^[A-Z][a-z]{3}$')) - - - def _createTrack(self, trackNode): - track = self._factory.newTrack() - track.setId(_getIdAttr(trackNode, 'id', 'track')) - - for node in _getChildElements(trackNode): - if _matches(node, 'title'): - track.setTitle(_getText(node)) - elif _matches(node, 'artist'): - track.setArtist(self._createArtist(node)) - elif _matches(node, 'duration'): - track.setDuration(_getPositiveIntText(node)) - elif _matches(node, 'release-list'): - self._addReleasesToList(node, track.getReleases()) - elif _matches(node, 'puid-list'): - self._addPuids(node, track) - elif _matches(node, 'isrc-list'): - self._addISRCs(node, track) - elif _matches(node, 'relation-list'): - self._addRelationsToEntity(node, track) - elif _matches(node, 'tag-list'): - self._addTagsToEntity(node, track) - elif _matches(node, 'rating'): - self._addRatingToEntity(node, track) - - return track - - # MusicBrainz extension - def _createUser(self, userNode): - user = self._factory.newUser() - for t in _getUriListAttr(userNode, 'type', NS_EXT_1): - user.addType(t) - - for node in _getChildElements(userNode): - if _matches(node, 'name'): - user.setName(_getText(node)) - elif _matches(node, 'nag', NS_EXT_1): - user.setShowNag(_getBooleanAttr(node, 'show')) - - return user - - def _createRating(self, ratingNode): - rating = self._factory.newRating() - rating.value = _getText(ratingNode) - rating.count = _getIntAttr(ratingNode, 'votes-count') - return rating - - def _createTag(self, tagNode): - tag = self._factory.newTag() - tag.value = _getText(tagNode) - tag.count = _getIntAttr(tagNode, 'count') - return tag - - - def _addPuids(self, puidListNode, track): - for node in _getChildElements(puidListNode): - if _matches(node, 'puid') and node.hasAttribute('id'): - track.addPuid(node.getAttribute('id')) - - def _addISRCs(self, isrcListNode, track): - for node in _getChildElements(isrcListNode): - if _matches(node, 'isrc') and node.hasAttribute('id'): - track.addISRC(node.getAttribute('id')) - - def _addRelationsToEntity(self, relationListNode, entity): - targetType = _getUriAttr(relationListNode, 'target-type', NS_REL_1) - - if targetType is None: - return - - for node in _getChildElements(relationListNode): - if _matches(node, 'relation'): - rel = self._createRelation(node, targetType) - if rel is not None: - entity.addRelation(rel) - - - def _createRelation(self, relationNode, targetType): - relation = self._factory.newRelation() - - relation.setType(_getUriAttr(relationNode, 'type', NS_REL_1)) - relation.setTargetType(targetType) - resType = _getResourceType(targetType) - relation.setTargetId(_getIdAttr(relationNode, 'target', resType)) - - if relation.getType() is None \ - or relation.getTargetType() is None \ - or relation.getTargetId() is None: - return None - - relation.setDirection(_getDirectionAttr(relationNode, 'direction')) - relation.setBeginDate(_getDateAttr(relationNode, 'begin')) - relation.setEndDate(_getDateAttr(relationNode, 'end')) - - for a in _getUriListAttr(relationNode, 'attributes', NS_REL_1): - relation.addAttribute(a) - - target = None - children = _getChildElements(relationNode) - if len(children) > 0: - node = children[0] - if _matches(node, 'artist'): - target = self._createArtist(node) - elif _matches(node, 'release'): - target = self._createRelease(node) - elif _matches(node, 'track'): - target = self._createTrack(node) - - relation.setTarget(target) - - return relation - - -# -# XML output -# - -class _XmlWriter(object): - def __init__(self, outStream, indentAmount=' ', newline="\n"): - self._out = outStream - self._indentAmount = indentAmount - self._stack = [ ] - self._newline = newline - - def prolog(self, encoding='UTF-8', version='1.0'): - pi = '' % (version, encoding) - self._out.write(pi + self._newline) - - def start(self, name, attrs={ }): - indent = self._getIndention() - self._stack.append(name) - self._out.write(indent + self._makeTag(name, attrs) + self._newline) - - def end(self): - name = self._stack.pop() - indent = self._getIndention() - self._out.write('%s\n' % (indent, name)) - - def elem(self, name, value, attrs={ }): - # delete attributes with an unset value - for (k, v) in attrs.items(): - if v is None or v == '': - del attrs[k] - - if value is None or value == '': - if len(attrs) == 0: - return - self._out.write(self._getIndention()) - self._out.write(self._makeTag(name, attrs, True) + '\n') - else: - escValue = saxutils.escape(value or '') - self._out.write(self._getIndention()) - self._out.write(self._makeTag(name, attrs)) - self._out.write(escValue) - self._out.write('\n' % name) - - def _getIndention(self): - return self._indentAmount * len(self._stack) - - def _makeTag(self, name, attrs={ }, close=False): - ret = '<' + name - - for (k, v) in attrs.iteritems(): - if v is not None: - v = saxutils.quoteattr(str(v)) - ret += ' %s=%s' % (k, v) - - if close: - return ret + '/>' - else: - return ret + '>' - - - -class MbXmlWriter(object): - """Write XML in the Music Metadata XML format.""" - - def __init__(self, indentAmount=' ', newline="\n"): - """Constructor. - - @param indentAmount: the amount of whitespace to use per level - """ - self._indentAmount = indentAmount - self._newline = newline - - - def write(self, outStream, metadata): - """Writes the XML representation of a Metadata object to a file. - - @param outStream: an open file-like object - @param metadata: a L{Metadata} object - """ - xml = _XmlWriter(outStream, self._indentAmount, self._newline) - - xml.prolog() - xml.start('metadata', { - 'xmlns': NS_MMD_1, - 'xmlns:ext': NS_EXT_1, - }) - - self._writeArtist(xml, metadata.getArtist()) - self._writeRelease(xml, metadata.getRelease()) - self._writeReleaseGroup(xml, metadata.getReleaseGroup()) - self._writeTrack(xml, metadata.getTrack()) - self._writeLabel(xml, metadata.getLabel()) - - if len(metadata.getArtistResults()) > 0: - xml.start('artist-list', { - 'offset': metadata.artistResultsOffset, - 'count': metadata.artistResultsCount, - }) - for result in metadata.getArtistResults(): - self._writeArtist(xml, result.getArtist(), - result.getScore()) - xml.end() - - if len(metadata.getReleaseResults()) > 0: - xml.start('release-list', { - 'offset': metadata.releaseResultsOffset, - 'count': metadata.releaseResultsCount, - }) - for result in metadata.getReleaseResults(): - self._writeRelease(xml, result.getRelease(), - result.getScore()) - xml.end() - - if len(metadata.getReleaseGroupResults()) > 0: - xml.start('release-group-list', { - 'offset': metadata.releaseGroupResultsOffset, - 'count': metadata.releaseGroupResultsCount - }) - for result in metadata.getReleaseGroupResults(): - self._writeReleaseGroup(xml, result.getReleaseGroup(), - result.getScore()) - xml.end() - - if len(metadata.getTrackResults()) > 0: - xml.start('track-list', { - 'offset': metadata.trackResultsOffset, - 'count': metadata.trackResultsCount, - }) - for result in metadata.getTrackResults(): - self._writeTrack(xml, result.getTrack(), - result.getScore()) - xml.end() - - if len(metadata.getLabelResults()) > 0: - xml.start('label-list', { - 'offset': metadata.labelResultsOffset, - 'count': metadata.labelResultsCount, - }) - for result in metadata.getLabelResults(): - self._writeLabel(xml, result.getLabel(), - result.getScore()) - xml.end() - - xml.end() - - - def _writeArtist(self, xml, artist, score=None): - if artist is None: - return - - xml.start('artist', { - 'id': mbutils.extractUuid(artist.getId()), - 'type': mbutils.extractFragment(artist.getType()), - 'ext:score': score, - }) - - xml.elem('name', artist.getName()) - xml.elem('sort-name', artist.getSortName()) - xml.elem('disambiguation', artist.getDisambiguation()) - xml.elem('life-span', None, { - 'begin': artist.getBeginDate(), - 'end': artist.getEndDate(), - }) - - if len(artist.getAliases()) > 0: - xml.start('alias-list') - for alias in artist.getAliases(): - xml.elem('alias', alias.getValue(), { - 'type': alias.getType(), - 'script': alias.getScript(), - }) - xml.end() - - if len(artist.getReleases()) > 0: - xml.start('release-list') - for release in artist.getReleases(): - self._writeRelease(xml, release) - xml.end() - - if len(artist.getReleaseGroups()) > 0: - xml.start('release-group-list') - for releaseGroup in artist.getReleaseGroups(): - self._writeReleaseGroup(xml, releaseGroup) - xml.end() - - self._writeRelationList(xml, artist) - # TODO: extensions - - xml.end() - - - def _writeRelease(self, xml, release, score=None): - if release is None: - return - - types = [mbutils.extractFragment(t) for t in release.getTypes()] - typesStr = None - if len(types) > 0: - typesStr = ' '.join(types) - - xml.start('release', { - 'id': mbutils.extractUuid(release.getId()), - 'type': typesStr, - 'ext:score': score, - }) - - xml.elem('title', release.getTitle()) - xml.elem('text-representation', None, { - 'language': release.getTextLanguage(), - 'script': release.getTextScript() - }) - xml.elem('asin', release.getAsin()) - - self._writeArtist(xml, release.getArtist()) - self._writeReleaseGroup(xml, release.getReleaseGroup()) - - if len(release.getReleaseEvents()) > 0: - xml.start('release-event-list') - for event in release.getReleaseEvents(): - self._writeReleaseEvent(xml, event) - xml.end() - - if len(release.getDiscs()) > 0: - xml.start('disc-list') - for disc in release.getDiscs(): - xml.elem('disc', None, { 'id': disc.getId() }) - xml.end() - - if len(release.getTracks()) > 0: - # TODO: count attribute - xml.start('track-list', { - 'offset': release.getTracksOffset() - }) - for track in release.getTracks(): - self._writeTrack(xml, track) - xml.end() - - self._writeRelationList(xml, release) - # TODO: extensions - - xml.end() - - def _writeReleaseGroup(self, xml, rg, score = None): - if rg is None: - return - - xml.start('release-group', { - 'id': mbutils.extractUuid(rg.getId()), - 'type': mbutils.extractFragment(rg.getType()), - 'ext:score': score, - }) - - xml.elem('title', rg.getTitle()) - self._writeArtist(xml, rg.getArtist()) - - if len(rg.getReleases()) > 0: - xml.start('release-list') - for rel in rg.getReleases(): - self._writeRelease(xml, rel) - xml.end() - - xml.end() - - def _writeReleaseEvent(self, xml, event): - xml.start('event', { - 'country': event.getCountry(), - 'date': event.getDate(), - 'catalog-number': event.getCatalogNumber(), - 'barcode': event.getBarcode(), - 'format': event.getFormat() - }) - - self._writeLabel(xml, event.getLabel()) - - xml.end() - - - def _writeTrack(self, xml, track, score=None): - if track is None: - return - - xml.start('track', { - 'id': mbutils.extractUuid(track.getId()), - 'ext:score': score, - }) - - xml.elem('title', track.getTitle()) - xml.elem('duration', str(track.getDuration())) - self._writeArtist(xml, track.getArtist()) - - if len(track.getReleases()) > 0: - # TODO: offset + count - xml.start('release-list') - for release in track.getReleases(): - self._writeRelease(xml, release) - xml.end() - - if len(track.getPuids()) > 0: - xml.start('puid-list') - for puid in track.getPuids(): - xml.elem('puid', None, { 'id': puid }) - xml.end() - - self._writeRelationList(xml, track) - # TODO: extensions - - xml.end() - - - def _writeLabel(self, xml, label, score=None): - if label is None: - return - - xml.start('label', { - 'id': mbutils.extractUuid(label.getId()), - 'type': mbutils.extractFragment(label.getType()), - 'ext:score': score, - }) - - xml.elem('name', label.getName()) - xml.elem('sort-name', label.getSortName()) - xml.elem('disambiguation', label.getDisambiguation()) - xml.elem('life-span', None, { - 'begin': label.getBeginDate(), - 'end': label.getEndDate(), - }) - - if len(label.getAliases()) > 0: - xml.start('alias-list') - for alias in label.getAliases(): - xml.elem('alias', alias.getValue(), { - 'type': alias.getType(), - 'script': alias.getScript(), - }) - xml.end() - - # TODO: releases, artists - - self._writeRelationList(xml, label) - # TODO: extensions - - xml.end() - - - def _writeRelationList(self, xml, entity): - for tt in entity.getRelationTargetTypes(): - xml.start('relation-list', { - 'target-type': mbutils.extractFragment(tt), - }) - for rel in entity.getRelations(targetType=tt): - self._writeRelation(xml, rel, tt) - xml.end() - - - def _writeRelation(self, xml, rel, targetType): - relAttrs = ' '.join([mbutils.extractFragment(a) - for a in rel.getAttributes()]) - - if relAttrs == '': - relAttrs = None - - attrs = { - 'type': mbutils.extractFragment(rel.getType()), - 'target': rel.getTargetId(), - 'direction': rel.getDirection(), - 'begin': rel.getBeginDate(), - 'end': rel.getBeginDate(), - 'attributes': relAttrs, - } - - if rel.getTarget() is None: - xml.elem('relation', None, attrs) - else: - xml.start('relation', attrs) - if targetType == NS_REL_1 + 'Artist': - self._writeArtist(xml, rel.getTarget()) - elif targetType == NS_REL_1 + 'Release': - self._writeRelease(xml, rel.getTarget()) - elif targetType == NS_REL_1 + 'Track': - self._writeTrack(xml, rel.getTarget()) - xml.end() - - -# -# DOM Utilities -# - -def _matches(node, name, namespace=NS_MMD_1): - """Checks if an xml.dom.Node and a given name and namespace match.""" - - if node.localName == name and node.namespaceURI == namespace: - return True - else: - return False - - -def _getChildElements(parentNode): - """Returns all direct child elements of the given xml.dom.Node.""" - - children = [ ] - for node in parentNode.childNodes: - if node.nodeType == node.ELEMENT_NODE: - children.append(node) - - return children - - -def _getText(element, regex=None, default=None): - """Returns the text content of the given xml.dom.Element. - - This function simply fetches all contained text nodes, so the element - should not contain child elements. - """ - res = '' - for node in element.childNodes: - if node.nodeType == node.TEXT_NODE: - res += node.data - - if regex is None or re.match(regex, res): - return res - else: - return default - - -def _getPositiveIntText(element): - """Returns the text content of the given xml.dom.Element as an int.""" - - res = _getText(element) - - if res is None: - return None - - try: - return int(res) - except ValueError: - return None - - -def _getAttr(element, attrName, regex=None, default=None, ns=None): - """Returns an attribute of the given element. - - If there is no attribute with that name or the attribute doesn't - match the regular expression, default is returned. - """ - if element.hasAttributeNS(ns, attrName): - content = element.getAttributeNS(ns, attrName) - - if regex is None or re.match(regex, content): - return content - else: - return default - else: - return default - - -def _getDateAttr(element, attrName): - """Gets an incomplete date from an attribute.""" - return _getAttr(element, attrName, '^\d+(-\d\d)?(-\d\d)?$') - - -def _getIdAttr(element, attrName, typeName): - """Gets an ID from an attribute and turns it into an absolute URI.""" - value = _getAttr(element, attrName) - - return _makeAbsoluteUri('http://musicbrainz.org/' + typeName + '/', value) - - - -def _getIntAttr(element, attrName, min=0, max=None, ns=None): - """Gets an int from an attribute, or None.""" - try: - val = int(_getAttr(element, attrName, ns=ns)) - - if max is None: - max = val - - if min <= val <= max: - return val - else: - return None - except ValueError: - return None # raised if conversion to int fails - except TypeError: - return None # raised if no such attribute exists - - -def _getUriListAttr(element, attrName, prefix=NS_MMD_1): - """Gets a list of URIs from an attribute.""" - if not element.hasAttribute(attrName): - return [ ] - - f = lambda x: x != '' - uris = filter(f, re.split('\s+', element.getAttribute(attrName))) - - m = lambda x: _makeAbsoluteUri(prefix, x) - uris = map(m, uris) - - return uris - - -def _getUriAttr(element, attrName, prefix=NS_MMD_1): - """Gets a URI from an attribute. - - This also works for space-separated URI lists. In this case, the - first URI is returned. - """ - uris = _getUriListAttr(element, attrName, prefix) - if len(uris) > 0: - return uris[0] - else: - return None - - -def _getBooleanAttr(element, attrName): - """Gets a boolean value from an attribute.""" - value = _getAttr(element, attrName) - if value == 'true': - return True - elif value == 'false': - return False - else: - return None - - -def _getDirectionAttr(element, attrName): - """Gets the Relation reading direction from an attribute.""" - regex = '^\s*(' + '|'.join(( - model.Relation.DIR_FORWARD, - model.Relation.DIR_BACKWARD)) + ')\s*$' - return _getAttr(element, 'direction', regex, model.Relation.DIR_NONE) - - -def _makeAbsoluteUri(prefix, uriStr): - """Creates an absolute URI adding prefix, if necessary.""" - if uriStr is None: - return None - - (scheme, netloc, path, params, query, frag) = urlparse.urlparse(uriStr) - - if scheme == '' and netloc == '': - return prefix + uriStr - else: - return uriStr - - -def _getResourceType(uri): - """Gets the resource type from a URI. - - The resource type is the basename of the URI's path. - """ - m = re.match('^' + NS_REL_1 + '(.*)$', uri) - - if m: - return m.group(1).lower() - else: - return None - -# EOF From 4b72f9f6a8715077d04c752041150b5140745811 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Mon, 28 May 2012 23:03:27 +0530 Subject: [PATCH 12/13] Added headphones authorization, fixed a few default values in musicbrainzngs, changed user agent for headphones. Much thanks to diskir for doing all the heavy lifting on this --- headphones/mb.py | 13 ++++++++----- lib/musicbrainzngs/musicbrainz.py | 17 ++++++++++++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index c4072d51..3c1d4312 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -8,6 +8,7 @@ from headphones import logger, db from headphones.helpers import multikeysort, replace_all import lib.musicbrainzngs as musicbrainzngs +from lib.musicbrainzngs import WebServiceError mb_lock = threading.Lock() @@ -42,17 +43,19 @@ def startmb(forcemb=False): mbport = 5000 sleepytime = 0 - musicbrainzngs.set_useragent("headphones","0.0","https://github.com/doskir/headphones") + musicbrainzngs.set_useragent("headphones","0.0","https://github.com/rembo10/headphones") musicbrainzngs.set_hostname(mbhost + ":" + str(mbport)) if sleepytime == 0: musicbrainzngs.set_rate_limit(False) else: musicbrainzngs.set_rate_limit(True) - #CHECK THIS - if mbuser and mbpass:#i have no idea if this will work or not - musicbrainzngs.auth(mbuser,mbpass) - #CHECK THIS + # Add headphones credentials + if headphones.MIRROR == "headphones": + if not mbuser and mbpass: + logger.warn("No username or password set for VIP server") + else: + musicbrainzngs.hpauth(mbuser,mbpass) #q = musicbrainzngs q = musicbrainzngs diff --git a/lib/musicbrainzngs/musicbrainz.py b/lib/musicbrainzngs/musicbrainz.py index 0fa63b56..c5a3d65e 100644 --- a/lib/musicbrainzngs/musicbrainz.py +++ b/lib/musicbrainzngs/musicbrainz.py @@ -10,6 +10,7 @@ import logging import socket import xml.etree.ElementTree as etree from xml.parsers import expat +import base64 from lib.musicbrainzngs import mbxml from lib.musicbrainzngs import util @@ -228,6 +229,14 @@ def auth(u, p): global user, password user = u password = p + +def hpauth(u, p): + """Set the username and password to be used in subsequent queries to + the MusicBrainz XML API that require authentication. + """ + global hpuser, hppassword + hpuser = u + hppassword = p def set_useragent(app, version, contact=None): """Set the User-Agent to be used for requests to the MusicBrainz webservice. @@ -357,7 +366,7 @@ class _MusicbrainzHttpRequest(compat.Request): # Core (internal) functions for calling the MB API. -def _safe_open(opener, req, body=None, max_retries=8, retry_delay_delta=2.0): +def _safe_open(opener, req, body=None, max_retries=3, 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 @@ -477,6 +486,12 @@ def _mb_request(path, method='GET', auth_required=False, client_required=False, # Make request. req = _MusicbrainzHttpRequest(method, url, data) req.add_header('User-Agent', _useragent) + + # Add headphones credentials + if hostname == '178.63.142.150:8181': + base64string = base64.encodestring('%s:%s' % (hpuser, hppassword)).replace('\n', '') + req.add_header("Authorization", "Basic %s" % base64string) + _log.debug("requesting with UA %s" % _useragent) if body: req.add_header('Content-Type', 'application/xml; charset=UTF-8') From 6ade134396198ef95268ea276676922f8232dae6 Mon Sep 17 00:00:00 2001 From: rembo10 Date: Tue, 29 May 2012 14:45:23 +0530 Subject: [PATCH 13/13] Removed multiple attempts to query musicbrainz from the headphones side as this is now taken care of from within musicbrainzngs. Fixed some tab/whitespace issues and fixed up some comments --- headphones/mb.py | 163 ++++++++++++++++++----------------------------- 1 file changed, 63 insertions(+), 100 deletions(-) diff --git a/headphones/mb.py b/headphones/mb.py index 3c1d4312..3140292b 100644 --- a/headphones/mb.py +++ b/headphones/mb.py @@ -68,7 +68,6 @@ def findArtist(name, limit=1): with mb_lock: artistlist = [] - attempt = 0 artistResults = None chars = set('!?*') @@ -76,15 +75,12 @@ def findArtist(name, limit=1): 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) + + try: + artistResults = musicbrainzngs.search_artists(query=name,limit=limit)['artist-list'] + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) + time.sleep(5) time.sleep(sleepytime) @@ -123,7 +119,6 @@ def findRelease(name, limit=1): with mb_lock: releaselistngs = [] - attempt = 0 releaseResultsngs = None chars = set('!?') @@ -132,15 +127,11 @@ def findRelease(name, limit=1): q, sleepytime = startmb(forcemb=True) - while attempt < 5: - - try: - releaseResultsngs = musicbrainzngs.search_releases(query=name,limit=limit)['release-list'] - break - except WebServiceError, e: #need to update exceptions - logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) - attempt += 1 - time.sleep(10) + try: + releaseResultsngs = musicbrainzngs.search_releases(query=name,limit=limit)['release-list'] + except WebServiceError, e: #need to update exceptions + logger.warn('Attempt to query MusicBrainz for "%s" failed: %s' % (name, str(e))) + time.sleep(5) time.sleep(sleepytime) @@ -164,28 +155,22 @@ def getArtist(artistid, extrasonly=False): artist_dict = {} artist = None - attempt = 0 q, sleepytime = startmb() - while attempt < 5: - - try: - limit = 100 - artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] - newRgs = None - artist['release-group-list'] = [] - while newRgs == None or len(newRgs) >= limit: - newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list'] - artist['release-group-list'] += newRgs - 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) - except Exception,e: - pass - + try: + limit = 100 + artist = musicbrainzngs.get_artist_by_id(artistid)['artist'] + newRgs = None + artist['release-group-list'] = [] + while newRgs == None or len(newRgs) >= limit: + newRgs = musicbrainzngs.browse_release_groups(artistid,release_type="album",offset=len(artist['release-group-list']),limit=limit)['release-group-list'] + artist['release-group-list'] += newRgs + except WebServiceError, e: + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) + time.sleep(5) + except Exception,e: + pass if not artist: return False @@ -235,16 +220,15 @@ def getArtist(artistid, extrasonly=False): if includeExtras or headphones.INCLUDE_EXTRAS: includes = ["single", "ep", "compilation", "soundtrack", "live", "remix"] for include in includes: + artist = None - attempt = 0 - while attempt < 5:#this may be redundant with musicbrainzngs, it seems to retry and wait by itself, i will leave it in for rembo to review - try: - artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist'] - break - except WebServiceError, e:#update exceptions - logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) - attempt += 1 - time.sleep(5) + + try: + artist = musicbrainzngs.get_artist_by_id(artistid,includes=["releases","release-groups"],release_status=['official'],release_type=include)['artist'] + except WebServiceError, e:#update exceptions + logger.warn('Attempt to retrieve artist information from MusicBrainz failed for artistid: %s (%s)' % (artistid, str(e))) + time.sleep(5) + if not artist: continue for rg in artist['release-group-list']: @@ -268,19 +252,14 @@ def getReleaseGroup(rgid): releaselist = [] releaseGroup = None - attempt = 0 q, sleepytime = startmb() - while attempt < 5: - - try: - releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group'] - 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) + try: + releaseGroup = musicbrainzngs.get_release_group_by_id(rgid,["artists","releases","media","discids",])['release-group'] + except WebServiceError, e: + logger.warn('Attempt to retrieve information from MusicBrainz for release group "%s" failed (%s)' % (rgid, str(e))) + time.sleep(5) if not releaseGroup: return False @@ -292,15 +271,12 @@ def getReleaseGroup(rgid): # to get more detailed release info (ASIN, track count, etc.) for release in releaseGroup['release-list']: releaseResult = None - attempt = 0 - while attempt < 5: - try: - releaseResult = musicbrainzngs.get_release_by_id(release['id'],["recordings","media"])['release'] - break - except WebServiceError, e: #UPDATE THIS - logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e))) - attempt += 1 - time.sleep(5) + + try: + releaseResult = musicbrainzngs.get_release_by_id(release['id'],["recordings","media"])['release'] + except WebServiceError, e: + logger.warn('Attempt to retrieve release information for %s from MusicBrainz failed (%s)' % (releaseResult.title, str(e))) + time.sleep(5) if not releaseResult: continue @@ -329,7 +305,7 @@ def getReleaseGroup(rgid): try: format = int(formats[releaseResult['medium-list'][0]['format']]) except: - format = 3 #this is the same number 'Cassette' uses above, change it ? + format = 3 try: country = int(countries[releaseResult['country']]) @@ -398,19 +374,14 @@ def getRelease(releaseid): release = {} results = None - attempt = 0 q, sleepytime = startmb() - - while attempt < 5: - try: - results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release') - break - except WebServiceError, e: #update this - logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) - attempt += 1 - time.sleep(5) + try: + results = musicbrainzngs.get_release_by_id(releaseid,["artists","release-groups","media","recordings"]).get('release') + except WebServiceError, e: + logger.warn('Attempt to retrieve information from MusicBrainz for release "%s" failed (%s)' % (releaseid, str(e))) + time.sleep(5) if not results: return False @@ -467,22 +438,18 @@ def findArtistbyAlbum(name): term = '"'+artist['AlbumTitle']+'" AND artist:"'+name+'"' results = None - attempt = 0 q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - try: - results = musicbrainzngs.search_release_groups(term).get('release-group-list') - break - except WebServiceError, e: #update exceptions - logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) - attempt += 1 - time.sleep(10) + + try: + results = musicbrainzngs.search_release_groups(term).get('release-group-list') + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s failed (%s)' % (name, str(e))) + time.sleep(5) time.sleep(sleepytime) - if not results: + if not results: return False artist_dict = {} @@ -505,19 +472,15 @@ def findArtistbyAlbum(name): def findAlbumID(artist=None, album=None): results_ngs = None - attempt = 0 q, sleepytime = startmb(forcemb=True) - - while attempt < 5: - try: - term = '"'+album+'" AND artist:"'+artist+'"' - results_ngs = musicbrainzngs.search_release_groups(term,1).get('release-group-list') - break - except WebServiceError, e:#update exceptions - logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) - attempt += 1 - time.sleep(10) + + try: + term = '"'+album+'" AND artist:"'+artist+'"' + results_ngs = musicbrainzngs.search_release_groups(term,1).get('release-group-list') + except WebServiceError, e: + logger.warn('Attempt to query MusicBrainz for %s - %s failed (%s)' % (artist, album, str(e))) + time.sleep(5) time.sleep(sleepytime)