python-musicbrainz2-0.7.4/0000755000175000017500000000000011655171144014476 5ustar lukaslukaspython-musicbrainz2-0.7.4/bin/0000755000175000017500000000000011655171140015242 5ustar lukaslukaspython-musicbrainz2-0.7.4/bin/mb-submit-disc0000755000175000017500000000155010427117514020011 0ustar lukaslukas#! /usr/bin/env python # # Helper script to calculate a MusicBrainz DiscID and a disc submission URL. # # Usage: # mb-submit-disc [device] # # $Id$ # import sys import musicbrainz2.disc as mbdisc if len(sys.argv) == 1: device = None elif len(sys.argv) == 2: device = sys.argv[1] if len(sys.argv) > 2: print >>sys.stderr, 'Usage: %s [device]' % sys.argv[0] sys.exit(1); try: # Read the disc in the given drive. If no device name is given, use # the default one. # if device is None: disc = mbdisc.readDisc() else: disc = mbdisc.readDisc(device) except mbdisc.DiscError, e: print "%s: Couldn't read disc: %s" % (sys.argv[0], e) print >>sys.stderr, "Is there an Audio CD in the drive?" sys.exit(1) print 'MusicBrainz DiscID:', disc.id print print 'You can use the following URL to submit this disc to MB:' print ' ', mbdisc.getSubmissionUrl(disc) # EOF python-musicbrainz2-0.7.4/examples/0000755000175000017500000000000011655171140016310 5ustar lukaslukaspython-musicbrainz2-0.7.4/examples/getuser.py0000755000175000017500000000141410411775001020336 0ustar lukaslukas#! /usr/bin/env python # # Display data about a MusicBrainz user (user name and password required). # # Usage: # python user.py # # $Id: getuser.py 201 2006-03-27 14:43:13Z matt $ # import sys import logging import getpass from musicbrainz2.webservice import WebService, WebServiceError, Query logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) user = raw_input('User name: ') passwd = getpass.getpass('Password: ') try: ws = WebService(host='musicbrainz.org', port=80, username=user, password=passwd) q = Query(ws) user = q.getUserByName(user) except WebServiceError, e: print 'Error:', e sys.exit(1) print 'Name :', user.name print 'ShowNag :', user.showNag print 'Types :', ' '.join(user.types) # EOF python-musicbrainz2-0.7.4/examples/findlabel.py0000755000175000017500000000227110657263546020624 0ustar lukaslukas#! /usr/bin/env python # # Search for a label by name. # # Usage: # python findlabel.py 'label-name' # # $Id: findlabel.py 9316 2007-08-11 07:38:14Z matt $ # import sys import logging from musicbrainz2.webservice import Query, LabelFilter, WebServiceError logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: findlabel.py 'label name'" sys.exit(1) q = Query() try: # Search for all labels matching the given name. Limit the results # to the 5 best matches. The offset parameter could be used to page # through the results. # f = LabelFilter(name=sys.argv[1], limit=5) labelResults = q.getLabels(f) except WebServiceError, e: print 'Error:', e sys.exit(1) # No error occurred, so display the results of the search. It consists of # LabelResult objects, where each contains a label. # for result in labelResults: label = result.label print "Score :", result.score print "Id :", label.id print "Name :", label.name print "Sort Name :", label.sortName print # # Now that you have label IDs, you can request a label directly to get more # detail. See 'getlabel.py' for an example on how to do that. # # EOF python-musicbrainz2-0.7.4/examples/getrelations.py0000755000175000017500000000464610430144330021366 0ustar lukaslukas#! /usr/bin/env python # # Retrieving an artist's relations to other artists and URLs. # # Usage: # python getrelations.py artist-id # # Interesting Artist IDs for testing: # http://musicbrainz.org/artist/ea4dfa26-f633-4da6-a52a-f49ea4897b58 # http://musicbrainz.org/artist/172e1f1a-504d-4488-b053-6344ba63e6d0 # http://musicbrainz.org/artist/c0b2500e-0cef-4130-869d-732b23ed9df5 # # $Id: getrelations.py 7496 2006-05-09 16:52:40Z matt $ # import sys import logging import musicbrainz2.webservice as ws import musicbrainz2.model as m logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: getrelations.py artist-id" sys.exit(1) q = ws.Query() try: # The result should include all relations to other artists and also # relations to URLs. # inc = ws.ArtistIncludes(artistRelations=True, releaseRelations=True, urlRelations=True) artist = q.getArtistById(sys.argv[1], inc) except ws.WebServiceError, e: print 'Error:', e sys.exit(1) print "Id :", artist.id print "Name :", artist.name print # # Get the artist's relations to URLs (m.Relation.TO_URL) having the relation # type 'http://musicbrainz.org/ns/rel-1.0#Wikipedia'. Note that there could # be more than one relation per type. We just print the first one. # urls = artist.getRelationTargets(m.Relation.TO_URL, m.NS_REL_1+'Wikipedia') if len(urls) > 0: print 'Wikipedia:', urls[0] print # # List discography pages for an artist. # for rel in artist.getRelations(m.Relation.TO_URL, m.NS_REL_1+'Discography'): print 'Discography:', rel.targetId print # # If the artist is a group, list all members. # if artist.type == m.Artist.TYPE_GROUP: allMembers = artist.getRelations(m.Relation.TO_ARTIST, m.NS_REL_1+'MemberOfBand') uri = m.NS_REL_1+'Additional' coreMembers = [r for r in allMembers if uri not in r.attributes] additionalMembers = [r for r in allMembers if uri in r.attributes] print 'Group members:' for rel in coreMembers: start = rel.beginDate or 'foundation' end = rel.endDate or 'end' print '\t%s (%s to %s)' % (rel.target.name, start, end) print print 'Additional members:' for rel in additionalMembers: print '\t', rel.target.name # # List all releases for which this artist has acted as the producer. # releases = artist.getRelationTargets(m.Relation.TO_RELEASE, m.NS_REL_1+'Producer') print print 'Credited as producer for:' for r in releases: print '\t', r.title # EOF python-musicbrainz2-0.7.4/examples/ripper.py0000755000175000017500000000410610411775001020162 0ustar lukaslukas#! /usr/bin/env python # # This shows the web service interaction needed for a typical CD ripper. # # Usage: # python ripper.py # # $Id: ripper.py 201 2006-03-27 14:43:13Z matt $ # import sys import logging import musicbrainz2.disc as mbdisc import musicbrainz2.webservice as mbws # Activate logging. # logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) # Setup a Query object. # service = mbws.WebService() query = mbws.Query(service) # Read the disc in the drive # try: disc = mbdisc.readDisc() except mbdisc.DiscError, e: print "Error:", e sys.exit(1) # Query for all discs matching the given DiscID. # try: filter = mbws.ReleaseFilter(discId=disc.getId()) results = query.getReleases(filter) except mbws.WebServiceError, e: print "Error:", e sys.exit(2) # No disc matching this DiscID has been found. # if len(results) == 0: print "Disc is not yet in the MusicBrainz database." print "Consider adding it via", mbdisc.getSubmissionUrl(disc) sys.exit(0) # Display the returned results to the user. # print 'Matching releases:' for result in results: release = result.release print 'Artist :', release.artist.name print 'Title :', release.title print # Select one of the returned releases. We just pick the first one. # selectedRelease = results[0].release # The returned release object only contains title and artist, but no tracks. # Query the web service once again to get all data we need. # try: inc = mbws.ReleaseIncludes(artist=True, tracks=True, releaseEvents=True) release = query.getReleaseById(selectedRelease.getId(), inc) except mbws.WebServiceError, e: print "Error:", e sys.exit(2) # Now display the returned data. # isSingleArtist = release.isSingleArtistRelease() print "%s - %s" % (release.artist.getUniqueName(), release.title) i = 1 for t in release.tracks: if isSingleArtist: title = t.title else: title = t.artist.name + ' - ' + t.title (minutes, seconds) = t.getDurationSplit() print " %2d. %s (%d:%02d)" % (i, title, minutes, seconds) i+=1 # All data has been retrieved, now actually rip the CD :-) # # EOF python-musicbrainz2-0.7.4/examples/getartist.py0000755000175000017500000000356511231304732020677 0ustar lukaslukas#! /usr/bin/env python # # Retrieve an artist by ID and display all official albums. # # Usage: # python getartist.py artist-id # # $Id: getartist.py 11853 2009-07-21 09:26:50Z luks $ # import sys import logging import musicbrainz2.webservice as ws import musicbrainz2.model as m logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: getartist.py artist-id" sys.exit(1) q = ws.Query() try: # The result should include all official albums. # inc = ws.ArtistIncludes( releases=(m.Release.TYPE_OFFICIAL, m.Release.TYPE_ALBUM), tags=True, releaseGroups=True) artist = q.getArtistById(sys.argv[1], inc) except ws.WebServiceError, e: print 'Error:', e sys.exit(1) print "Id :", artist.id print "Name :", artist.name print "SortName :", artist.sortName print "UniqueName :", artist.getUniqueName() print "Type :", artist.type print "BeginDate :", artist.beginDate print "EndDate :", artist.endDate print "Tags :", ', '.join([t.value for t in artist.tags]) print if len(artist.getReleases()) == 0: print "No releases found." else: print "Releases:" for release in artist.getReleases(): print print "Id :", release.id print "Title :", release.title print "ASIN :", release.asin print "Text :", release.textLanguage, '/', release.textScript print "Types :", release.types print if len(artist.getReleaseGroups()) == 0: print print "No release groups found." else: print print "Release groups:" for rg in artist.getReleaseGroups(): print print "Id :", rg.id print "Title :", rg.title print "Type :", rg.type print # # Using the release IDs and Query.getReleaseById(), you could now request # those releases, including the tracks, release events, the associated # DiscIDs, and more. The 'getrelease.py' example shows how this works. # # EOF python-musicbrainz2-0.7.4/examples/folksonomytags.py0000755000175000017500000000251010762216154021745 0ustar lukaslukas#! /usr/bin/env python # # This queries the tags a user has applied to an entity and submits a changed # list of tags to the MusicBrainz server. # # Usage: # python tag.py # # $Id: folksonomytags.py 9714 2008-03-01 09:05:48Z matt $ # import getpass import sys import logging import musicbrainz2.webservice as mbws from musicbrainz2.model import Tag from musicbrainz2.utils import extractUuid MB_HOST = 'test.musicbrainz.org' logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) # Get the username and password username = raw_input('Username: ') password = getpass.getpass('Password: ') # Ask for a MBID to tag. mbid = raw_input('Enter an absolute MB ID: ') # Set the authentication for the webservice (only needed for tag submission). service = mbws.WebService(host=MB_HOST, username=username, password=password) # Create a new Query object which will provide # us an interface to the MusicBrainz web service. query = mbws.Query(service) # Read and print the current tags for the given MBID tags = query.getUserTags(mbid) print print 'Current tags: ' print ', '.join([tag.value for tag in tags]) # Ask the user for new tags and submit them tag_str = raw_input('Enter new tags: ') new_tags = [Tag(tag.strip()) for tag in tag_str.split(',')] query.submitUserTags(mbid, new_tags) print 'Tags submitted.' # EOF python-musicbrainz2-0.7.4/examples/findtrack.py0000755000175000017500000000163210411775001020627 0ustar lukaslukas#! /usr/bin/env python # # Search for a track by title (and optionally by artist name). # # Usage: # python findtrack.py 'track name' ['artist name'] # # $Id: findtrack.py 201 2006-03-27 14:43:13Z matt $ # import sys import logging from musicbrainz2.webservice import Query, TrackFilter, WebServiceError logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: findtrack.py 'track name' ['artist name']" sys.exit(1) if len(sys.argv) > 2: artistName = sys.argv[2] else: artistName = None q = Query() try: f = TrackFilter(title=sys.argv[1], artistName=artistName) results = q.getTracks(f) except WebServiceError, e: print 'Error:', e sys.exit(1) for result in results: track = result.track print "Score :", result.score print "Id :", track.id print "Title :", track.title print "Artist :", track.artist.name print # EOF python-musicbrainz2-0.7.4/examples/getlabel.py0000755000175000017500000000177710657263546020475 0ustar lukaslukas#! /usr/bin/env python # # Retrieve a label by ID. # # Usage: # python getlabel.py label-id # # $Id: getlabel.py 9316 2007-08-11 07:38:14Z matt $ # import sys import logging import musicbrainz2.webservice as ws import musicbrainz2.model as m logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: getlabel.py label-id" sys.exit(1) q = ws.Query() try: # The result should include all aliases. # inc = ws.LabelIncludes(aliases=True) label = q.getLabelById(sys.argv[1], inc) except ws.WebServiceError, e: print 'Error:', e sys.exit(1) print "Id :", label.id print "Name :", label.name print "SortName :", label.sortName print "UniqueName :", label.getUniqueName() print "Type :", label.type print "BeginDate :", label.beginDate print "EndDate :", label.endDate print "Country :", label.country print "Label-Code :", label.code print "Aliases :" for alias in label.aliases: print " ", label.value # EOF python-musicbrainz2-0.7.4/examples/getrelease.py0000755000175000017500000000470111231304732021002 0ustar lukaslukas#! /usr/bin/env python # # Retrieve a release by ID and display it. # # Usage: # python getrelease.py release-id # # Interesting releases IDs for testing: # http://musicbrainz.org/release/290e10c5-7efc-4f60-ba2c-0dfc0208fbf5 # http://musicbrainz.org/release/fa9f1bdd-495f-41b9-8944-1a766da29120 # # $Id: getrelease.py 11853 2009-07-21 09:26:50Z luks $ # import sys import logging import musicbrainz2.webservice as ws import musicbrainz2.utils as u logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: getrelease.py release-id" sys.exit(1) q = ws.Query() try: # Additionally to the release itself, we want the server to include # the release's artist, all release events, associated discs and # the track list. # inc = ws.ReleaseIncludes(artist=True, releaseEvents=True, labels=True, discs=True, tracks=True, releaseGroup=True) release = q.getReleaseById(sys.argv[1], inc) except ws.WebServiceError, e: print 'Error:', e sys.exit(1) print "Id :", release.id print "Title :", release.title print "ASIN :", release.asin print "Lang/Script :", release.textLanguage, '/', release.textScript # Print the main artist of this release. # if release.artist: print print "Artist:" print " Id :", release.artist.id print " Name :", release.artist.name print " SortName :", release.artist.sortName if release.releaseGroup: print print "Release Group:" print " Id :", release.releaseGroup.id print " Title :", release.releaseGroup.title print " Type :", release.releaseGroup.type # Release events are the dates and times when a release took place. # We also have the catalog numbers and barcodes for some releases. # if len(release.releaseEvents) > 0: print print "Released (earliest: %s):" % release.getEarliestReleaseDate() for event in release.releaseEvents: print " %s %s" % (u.getCountryName(event.country), event.date), if event.catalogNumber: print '#' + event.catalogNumber, if event.barcode: print 'EAN=' + event.barcode, if event.label: print '(' + event.label.name + ')', print if len(release.discs) > 0: print print "Discs:" for disc in release.discs: print " DiscId: %s (%d sectors)" % (disc.id, disc.sectors) if len(release.tracks) > 0: print print "Tracks:" for track in release.tracks: print " Id :", track.id print " Title :", track.title print " Duration :", track.duration print # EOF python-musicbrainz2-0.7.4/examples/discid.py0000755000175000017500000000147610411775001020127 0ustar lukaslukas#! /usr/bin/env python # # Read a CD in the disc drive and calculate a MusicBrainz DiscID. # # Usage: # python discid.py # # $Id: discid.py 201 2006-03-27 14:43:13Z matt $ # import sys from musicbrainz2.disc import readDisc, getSubmissionUrl, DiscError try: # Read the disc in the default disc drive. If necessary, you can pass # the 'deviceName' parameter to select a different drive. # disc = readDisc() except DiscError, e: print "DiscID calculation failed:", str(e) sys.exit(1) print 'DiscID :', disc.id print 'First Track :', disc.firstTrackNum print 'Last Track :', disc.lastTrackNum print 'Length :', disc.sectors, 'sectors' i = disc.firstTrackNum for (offset, length) in disc.tracks: print "Track %-2d : %8d %8d" % (i, offset, length) i += 1 print 'Submit via :', getSubmissionUrl(disc) # EOF python-musicbrainz2-0.7.4/examples/findartist.py0000755000175000017500000000240110550142420021021 0ustar lukaslukas#! /usr/bin/env python # # Search for an artist by name. # # Usage: # python findartist.py 'artist-name' # # $Id: findartist.py 8779 2007-01-07 10:01:52Z matt $ # import sys import logging from musicbrainz2.webservice import Query, ArtistFilter, WebServiceError logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: findartist.py 'artist name'" sys.exit(1) q = Query() try: # Search for all artists matching the given name. Limit the results # to the 5 best matches. The offset parameter could be used to page # through the results. # f = ArtistFilter(name=sys.argv[1], limit=5) artistResults = q.getArtists(f) except WebServiceError, e: print 'Error:', e sys.exit(1) # No error occurred, so display the results of the search. It consists of # ArtistResult objects, where each contains an artist. # for result in artistResults: artist = result.artist print "Score :", result.score print "Id :", artist.id print "Name :", artist.name print "Sort Name :", artist.sortName print # # Now that you have artist IDs, you can request an artist in more detail, for # example to display all official albums by that artist. See the 'getartist.py' # example on how achieve that. # # EOF python-musicbrainz2-0.7.4/examples/README.txt0000644000175000017500000000345211231304732020005 0ustar lukaslukasExamples for typical use cases ------------------------------ This directory contains example code which should cover the most important use cases. The following demo scripts are available: discid.py Reads the disc in the computers CD-ROM/DVD-ROM drive. It calculates a MusicBrainz DiscID and displays the TOC as well as a submission URL. findartist.py Search MusicBrainz for artists matching the given name. findtrack.py Search MusicBrainz for a track matching the given track title and, optionally, artist name. findlabel.py Search MusicBrainz for a label matching the given label name. findreleasegroup.py Search MusicBrainz for a release group matching a Lucene query. folksonomytags.py Get and submit MusicBrainz folksonomy tags for a given entity. getartist.py Retrieve an artist using a MusicBrainz ID and display all officially released albums. getrelease.py Retrieve a release by MusicBrainz ID, including tracks and other associated data, like release dates or DiscIDs. getreleasegroup.py Retrieve a release group by MusicBrainz ID, including releases, primary artist and release types. getlabel.py Retrieve a label by MusicBrainz ID, including aliases. getrelations.py Retrieve an artist by MusicBrainz ID and display relations to URLs and other artists. This demonstrates how to use the more advanced features of the web service. getuser.py Display some information about a MusicBrainz user. You need the user name and password to use this. ripper.py This example shows how to get a MusicBrainz DiscID for a CD in the disc drive and to get the matching releases from the web service. This is the typical workflow for a simple CD ripper application. -- $Id: README.txt 11853 2009-07-21 09:26:50Z luks $ python-musicbrainz2-0.7.4/examples/findreleasegroup.py0000644000175000017500000000302411231304732022212 0ustar lukaslukas#! /usr/bin/env python # # Search for a release group by name. # # Usage: # python findreleasegroup.py 'album name/Lucene query' # # Examples: # findreleasegroup.py "Signal morning" # findreleasegroup.py '"Gimme Fiction" AND artist:"Spoon"' # findreleasegroup.py '"Skylarking" AND type:"Album"' # import sys import logging from musicbrainz2.webservice import Query, ReleaseGroupFilter, WebServiceError logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: findreleasegroup.py 'release group name/Lucene query'" sys.exit(1) q = Query() try: # Search for all release groups matching the given query. Limit the results # to the 5 best matches. The offset parameter could be used to page # through the results. # f = ReleaseGroupFilter(query=sys.argv[1], limit=5) results = q.getReleaseGroups(f) except WebServiceError, e: print 'Error:', e sys.exit(1) # No error occurred, so display the results of the search. It consists of # ReleaseGroupResult objects, where each contains an artist. # for result in results: releaseGroup = result.releaseGroup print "Score\t\t :", result.score print "Id :", releaseGroup.id print "Name :", releaseGroup.title print "Artist :", releaseGroup.artist.name print "Type :", releaseGroup.type print # # Now that you have release group IDs, you can request a release group in more detail, for # example to display all official releases in that group. See the 'getreleasegroup.py' # example on how achieve that. # # EOF python-musicbrainz2-0.7.4/examples/getreleasegroup.py0000644000175000017500000000327711231304732022063 0ustar lukaslukas#! /usr/bin/env python # # Retrieve a release group by ID and display it. # # Usage: # python getreleasegroup.py releasegroup-id # # Interesting release group IDs for testing: # http://musicbrainz.org/release-group/e49e2f8a-94c0-3dcf-8ce6-9bc52a1a7867 # http://musicbrainz.org/release-group/c7a0fc4d-b6a0-3a43-9e25-4052e4fe33b2 # http://musicbrainz.org/release-group/055be730-dcad-31bf-b550-45ba9c202aa3 # http://musicbrainz.org/release-group/963eac15-e3da-3a92-aa5c-2ec23bfb6ec2 # # import sys import logging import musicbrainz2.webservice as ws import musicbrainz2.utils as u logging.basicConfig() logger = logging.getLogger() logger.setLevel(logging.DEBUG) if len(sys.argv) < 2: print "Usage: getreleasegroup.py releasegroup-id" sys.exit(1) q = ws.Query() try: # Additionally to the release itself, we want the server to include # the release's artist, all release events, associated discs and # the track list. # inc = ws.ReleaseGroupIncludes(artist=True, releases=True, tags=False) releaseGroup = q.getReleaseGroupById(sys.argv[1], inc) except ws.WebServiceError, e: print 'Error:', e sys.exit(1) print "Id :", releaseGroup.id print "Title :", releaseGroup.title print "Type :", releaseGroup.type # Print the main artist of this release group. # if releaseGroup.artist: print print "Artist:" print " Id :", releaseGroup.artist.id print " Name :", releaseGroup.artist.name print " SortName :", releaseGroup.artist.sortName # Show the releases contained by this release group. # for release in releaseGroup.releases: print print "Release:" print " Id :", release.id print " Title :", release.title print " Types :", release.types # EOF python-musicbrainz2-0.7.4/test-data/0000755000175000017500000000000011655171146016366 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/0000755000175000017500000000000011655171146020014 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/release/0000755000175000017500000000000011655171146021434 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/track/0000755000175000017500000000000011655171146021120 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/artist/0000755000175000017500000000000011655171146021322 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/artist/empty_1.xml0000644000175000017500000000000010367360533023407 0ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/invalid/artist/tags_1.xml0000644000175000017500000000026410637777526023241 0ustar lukaslukas foo python-musicbrainz2-0.7.4/test-data/invalid/artist/empty_3.xml0000644000175000017500000000015010367360533023417 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/invalid/artist/empty_2.xml0000644000175000017500000000013710367360533023423 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/invalid/artist/ratings_2.xml0000644000175000017500000000024411104666317023732 0ustar lukaslukas 2.5 python-musicbrainz2-0.7.4/test-data/invalid/artist/basic_1.xml0000644000175000017500000000033010367360533023340 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/invalid/artist/ratings_1.xml0000644000175000017500000000025111104666317023727 0ustar lukaslukas 2 python-musicbrainz2-0.7.4/test-data/invalid/artist/basic_2.xml0000644000175000017500000000037010367360533023345 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/invalid/artist/search_result_1.xml0000644000175000017500000000065310410272540025117 0ustar lukaslukas Tori Amos Amos, Tori python-musicbrainz2-0.7.4/test-data/README0000644000175000017500000000066610367360533017255 0ustar lukaslukasMusicBrainz Web Service XML Examples ------------------------------------ This directory contains example XML code for testing an implementation. Implementations MUST be able to parse the XML in the 'valid' directory. It is perfect, schema-compliant XML. The 'invalid' directory contains all kinds of invalid XML. Implementations are not expected to parse those files. They MAY, however. -- $Id: README 33 2006-01-30 09:50:19Z matt $ python-musicbrainz2-0.7.4/test-data/valid/0000755000175000017500000000000011655171146017465 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/release/0000755000175000017500000000000011655171145021104 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/release/Mission_Impossible_2.xml0000644000175000017500000001601110405741741025652 0ustar lukaslukas Mission: Impossible 2 Various Artists Various Artists Take a Look Around 320160 Limp Bizkit Limp Bizkit I Disappear 266306 Metallica Metallica Scum of the Earth 175533 Rob Zombie Zombie, Rob They Came In 282560 Butthole Surfers Butthole Surfers Rocket Science 210466 The Pimps Pimps, The Have a Cigar (feat. Brian May) 242933 Foo Fighters Foo Fighters Mission 2000 222440 Chris Cornell Cornell, Chris Going Down 203160 Godsmack Godsmack What U Lookin' At? 312000 Uncle Kracker Uncle Kracker Backwards 189506 Apartment 26 Apartment 26 Karma 196160 Diffuser Diffuser Alone 203973 Buckcherry Buckcherry Immune 229026 Tinfed Tinfed My Kinda Scene 266666 Powderfinger Powderfinger Australian rock band Carnival 258066 Tori Amos Amos, Tori Nyah (feat. Heitor Pereira) 213306 Hans Zimmer Zimmer, Hans python-musicbrainz2-0.7.4/test-data/valid/release/Under_the_Pink_3.xml0000644000175000017500000000123210675465612024752 0ustar lukaslukas Under the Pink Tori Amos python-musicbrainz2-0.7.4/test-data/valid/release/Under_the_Pink_2.xml0000644000175000017500000000124611226063413024740 0ustar lukaslukas Under the Pink Tori Amos Under the Pink God python-musicbrainz2-0.7.4/test-data/valid/release/Little_Earthquakes_2.xml0000644000175000017500000000574210371675144025653 0ustar lukaslukas Little Earthquakes B000002IT2 Tori Amos Amos, Tori Crucify 301186 Girl 249493 Silent All These Years 253466 Precious Things 269293 Winter 344573 Happy Phantom 199000 China 301946 Leather 193960 Mother 421173 Tear in Your Hand 280866 Me and a Gun 226920 Little Earthquakes 413693 python-musicbrainz2-0.7.4/test-data/valid/release/Little_Earthquakes_1.xml0000644000175000017500000000174010371365467025650 0ustar lukaslukas Little Earthquakes B000002IT2 Tori Amos Amos, Tori python-musicbrainz2-0.7.4/test-data/valid/release/Under_the_Pink_1.xml0000644000175000017500000000113210410300474024724 0ustar lukaslukas Under the Pink B000002IXU Tori Amos python-musicbrainz2-0.7.4/test-data/valid/release/Highway_61_Revisited_2.xml0000644000175000017500000000117011104666317025771 0ustar lukaslukas Highway 61 Revisited rock blues rock folk rock dylan rock foo 4.5 3 python-musicbrainz2-0.7.4/test-data/valid/release/search_result_1.xml0000644000175000017500000000223610550166442024711 0ustar lukaslukas Under the Pink B000002IXU Tori Amos Under the Pink Tour 1994 Tori Amos python-musicbrainz2-0.7.4/test-data/valid/release/Highway_61_Revisited_1.xml0000644000175000017500000000575010405051643025771 0ustar lukaslukas Highway 61 Revisited B0000C8AVR Bob Dylan Dylan, Bob Like a Rolling Stone 373333 Tombstone Blues 360533 It Takes a Lot to Laugh, It Takes a Train to Cry 249066 From a Buick 6 199026 Ballad of a Thin Man 358640 Queen Jane Approximately 331760 Highway 61 Revisited 210333 Just Like Tom Thumb's Blues 332133 Desolation Row 682506 python-musicbrainz2-0.7.4/test-data/valid/track/0000755000175000017500000000000011655171145020570 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_4.xml0000644000175000017500000000235311215136226026052 0ustar lukaslukas Silent All These Years 253466 Tori Amos Amos, Tori Little Earthquakes python-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_3.xml0000644000175000017500000000121410405051643026043 0ustar lukaslukas Silent All These Years 253466 python-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_5.xml0000644000175000017500000000142710405051643026053 0ustar lukaslukas Silent All These Years 253466 This is a very nice song. python-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_6.xml0000644000175000017500000000073311201061057026046 0ustar lukaslukas Silent All These Years 253466 foo 4.5 3 python-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_1.xml0000644000175000017500000000036010367360533026052 0ustar lukaslukas Silent All These Years 253466 python-musicbrainz2-0.7.4/test-data/valid/track/Silent_All_These_Years_2.xml0000644000175000017500000000136310406316150026045 0ustar lukaslukas Silent All These Years 253466 Tori Amos Amos, Tori Silent All These Years python-musicbrainz2-0.7.4/test-data/valid/track/search_result_1.xml0000644000175000017500000000354510550166442024401 0ustar lukaslukas Little Earthquakes 457760 Tori Amos To Venus and Back (disc 2: Live, Still Orbiting) Little Earthquakes 413693 Tori Amos Little Earthquakes Little Amsterdam 270106 Tori Amos Boys for Pele python-musicbrainz2-0.7.4/test-data/valid/artist/0000755000175000017500000000000011655171146020773 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/artist/Tori_Amos_5.xml0000644000175000017500000000102210477304634023631 0ustar lukaslukas Tori Amos Amos, Tori Strange Little Girls python-musicbrainz2-0.7.4/test-data/valid/artist/empty_1.xml0000644000175000017500000000021710367360533023072 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/artist/Tchaikovsky-1.xml0000644000175000017500000001075710372357332024162 0ustar lukaslukas Пётр Ильич Чайковский Tchaikovsky, Pyotr Ilyich Tchaikovsky Peter Ilyich Tchaikovsky Peter Tchaikovsky Tschaikowsky Peter Tschaikowsky Piotr Ilyich Tchaikovsky Piotr Tchaikovsky Peter Iljitsch Tschaikowsky Tchaikovsky, Peter Ilyich Peter Ilyitch Tchaikovsky Pjotr Ilyich Tchaikovsky Peter I. Tschaikowsky Pyotr Tchaikovsky P. I. Tchaikovsky Peter Ilich Tchaikovsky Tchaikovsky, P.I. Tsjaikovski Tchaikovsky, Pyotr Ilyich Tjajkovskij Piotr Ilyitch Tchaikovsky Tsjajkovskij Peter Ilyich Tchaikovski Peter I. Tchaikovsky Tchaikovsky, Peter I. Tchaikowsky Peter Ilyich Tschaikowsky Peter Iljitsch Tschaikowski Pyotr Il'Yich Tchaikovsky Tchiakovsky, Pyotr Ilich (1840-1893) Tchaikovsky, Peter Ilyich (1840-93) Tchaikovsky, Peter Ilyitch Pyotr Ilyitch Tchaikovsky Tsaikovski Pytor Ilyich Tchaikovsky Piotr Ilyich Tchaikowsky Tchaikovsky - Philharmonic Orchestra Peter Iljitsch Tschaikowsky (1840 - 1893) Peter Tschaikovsky Peter Ilych Tschaikowsky Pyotr II'yich Tchaikovsky Pytor Tchaikovsky Pyotr Ilyich Tchaikovsky Чайковский, Петр Ильич Чайковский, Пётр Ильич Петр Ильич Чайковский Пётр Ильич Чайковский Tchaikovsky, Peter Il'yich Tchaikovsky, Pjotr Ilyich (1840 - 1893) Piotr Illitch Tchaïkovsky Piotr Ilic Ciaikovsky Pyotr Illyich Tchaikovsky Tchaikovsky, Piotr Ilich (1840-1893) Peter Ilyich Tchaikovshy Pyotr Ilyich Tchaikovsly Peter Ilych Tchaikovsky 차이코프스키 Piotr Ilitch Tchaïkovski Chaikovsky, P. I. Pjotr Iljitsch Tschaikowsky Ciaikosvsky Tchaikovsky 1841-1893 Tchaïkovki Piotr Ilych Chaikovsky Pjotr Iljitsj Tsjaikovski Pyotor Ilyich Tschaikovsky Peter Iljitsj Tsjaikovski P. I. Tchaikovskij Piotr Ilich Tchaikovsky Peter Iljitsch Tchaikovsky Tchaikovisky Tchaikovsy Tchailovisky Tchaikovskyes Tchaikovskys Tchaikoskvy Piotr Il'yich Tchaikovsky Tchaikowski Piotr Iljič Čajkovskij Tchaikivsky Pyotor Tchaikovsky python-musicbrainz2-0.7.4/test-data/valid/artist/empty_2.xml0000644000175000017500000000026210367360533023073 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/artist/Tori_Amos_4.xml0000644000175000017500000000105510406300610023614 0ustar lukaslukas Tori Amos Amos, Tori yes, that one Myra Ellen Amos Myra Amos Torie Amos python-musicbrainz2-0.7.4/test-data/valid/artist/Tchaikovsky-2.xml0000644000175000017500000000126711104666317024157 0ustar lukaslukas Пётр Ильич Чайковский classical russian romantic era composer classical russian 4.5 3 python-musicbrainz2-0.7.4/test-data/valid/artist/Tori_Amos_2.xml0000644000175000017500000000651311226063413023626 0ustar lukaslukas Tori Amos Amos, Tori Strange Little Girls B00005NKYQ To Venus and Back (disc 1: Orbiting) B00001IVJS Under the Pink B000002IXU Under the Pink To Venus and Back Strange Little Girls python-musicbrainz2-0.7.4/test-data/valid/artist/Tori_Amos_3.xml0000644000175000017500000000162310437077165023640 0ustar lukaslukas Tori Amos Amos, Tori Mark Hawley Hawley, Mark python-musicbrainz2-0.7.4/test-data/valid/artist/Tori_Amos_1.xml0000644000175000017500000000045510367360533023634 0ustar lukaslukas Tori Amos Amos, Tori python-musicbrainz2-0.7.4/test-data/valid/artist/search_result_1.xml0000644000175000017500000000147410430176546024605 0ustar lukaslukas Tori Amos Amos, Tori Tori Spelling Spelling, Tori Lisa And Tori Lisa And Tori python-musicbrainz2-0.7.4/test-data/valid/label/0000755000175000017500000000000011655171145020543 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/label/Atlantic_Records_2.xml0000644000175000017500000000070310637777526024744 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/label/Atlantic_Records_3.xml0000644000175000017500000000101111104666317024717 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/label/Atlantic_Records_1.xml0000644000175000017500000000052110607354732024724 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/label/search_result_1.xml0000644000175000017500000000107510607354732024354 0ustar lukaslukas python-musicbrainz2-0.7.4/test-data/valid/release-group/0000755000175000017500000000000011655171146022237 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/release-group/The_Cure_1.xml0000644000175000017500000000271011214414205024662 0ustar lukaslukas The Cure The Cure Cure, The The Cure B0002CHGZI The Cure The Cure B0002C9G7O The Cure B00028HOFY python-musicbrainz2-0.7.4/test-data/valid/release-group/search_result_1.xml0000644000175000017500000000212611226063413026034 0ustar lukaslukas Signal Morning Circulatory System Circulatory System Circulatory System Inside Views Circulatory System python-musicbrainz2-0.7.4/test-data/valid/user/0000755000175000017500000000000011655171146020443 5ustar lukaslukaspython-musicbrainz2-0.7.4/test-data/valid/user/User_1.xml0000644000175000017500000000061510374153130022313 0ustar lukaslukas matt python-musicbrainz2-0.7.4/COPYING.txt0000644000175000017500000000273710434316632016355 0ustar lukaslukasCopyright (c) 2006, Matthias Friedrich All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the MusicBrainz project nor the names of the contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. python-musicbrainz2-0.7.4/AUTHORS.txt0000644000175000017500000000214111347471065016365 0ustar lukaslukasAuthors: Matthias Friedrich - Designed, implemented and documented this package. Contributors: Lukáš Lalinský - Fixed several bugs and tested windows/ctypes compatibility. - Patch for changing TRMs to PUIDs. - Created and submitted Debian/Ubuntu packages. - Lots of other things, including patch reviews. Alastair Porter - Added ISRC support. - Added support for user collections. - Added CD Stub submission support. Nikki - Tested compatibility with python-2.3 on Debian. - Provided country/language/script lists. Oliver Charles - Added initial label support. Philipp Wolfer - Added folksonomy tagging support. Peter Schnebel - Added rating support. John J. Jordan - Added Release Groups support. -- $Id: AUTHORS.txt 12708 2010-03-15 17:45:25Z matt $ python-musicbrainz2-0.7.4/setup.py0000755000175000017500000000567411247217206016224 0ustar lukaslukas#! /usr/bin/env python __revision__ = '$Id: setup.py 12028 2009-09-01 13:15:50Z matt $' import os import sys import os.path import unittest from distutils.core import setup, Command sys.path.insert(0, 'src') import musicbrainz2 class TestCommand(Command): description = 'run all test cases' user_options = [ ] def initialize_options(self): pass def finalize_options(self): pass def run(self): files = [ ] for f in os.listdir('test'): elems = os.path.splitext(f) if f != '__init__.py' and elems[1] == '.py': files.append('test.' + elems[0]) tests = unittest.defaultTestLoader.loadTestsFromNames(files) t = unittest.TextTestRunner() t.run(tests) class GenerateDocsCommand(Command): description = 'generate the API documentation' user_options = [ ] def initialize_options(self): pass def finalize_options(self): pass def run(self): from distutils.spawn import find_executable, spawn bin = find_executable('epydoc') if not bin: print>>sys.stderr, 'error: epydoc not found' sys.exit(1) noPrivate = '--no-private' verbose = '-v' cmd = (bin, noPrivate, verbose, os.path.join('src', 'musicbrainz2')) spawn(cmd) long_description = """\ An interface to the MusicBrainz XML web service =============================================== python-musicbrainz2 provides simple, object oriented access to the MusicBrainz web service. It is useful for applications like CD rippers, taggers, media players, and other tools that need music metadata. The MusicBrainz Project (see http://musicbrainz.org) collects music metadata and is maintained by its large and constantly growing user community. Most of this package works on python-2.3 and later without further dependencies. If you want to generate DiscIDs from an audio CD in the drive, you need ctypes (already included in python-2.5) and libdiscid. """ trove_classifiers = [ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Topic :: Database :: Front-Ends', 'Topic :: Multimedia :: Sound/Audio :: CD Audio :: CD Ripping', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Text Processing :: Markup :: XML', ] setup_args = { 'name': 'python-musicbrainz2', 'version': musicbrainz2.__version__, 'description': 'An interface to the MusicBrainz XML web service', 'long_description': long_description, 'author': 'Matthias Friedrich', 'author_email': 'matt@mafr.de', 'url': 'http://musicbrainz.org/products/python-musicbrainz2/', 'download_url': 'http://ftp.musicbrainz.org/pub/musicbrainz/python-musicbrainz2/', 'classifiers': trove_classifiers, 'license': 'BSD', 'packages': [ 'musicbrainz2', 'musicbrainz2.data' ], 'package_dir': { 'musicbrainz2': 'src/musicbrainz2' }, 'scripts': [ 'bin/mb-submit-disc' ], 'cmdclass': { 'test': TestCommand, 'docs': GenerateDocsCommand }, } setup(**setup_args) # EOF python-musicbrainz2-0.7.4/src/0000755000175000017500000000000011655171136015266 5ustar lukaslukaspython-musicbrainz2-0.7.4/src/musicbrainz2/0000755000175000017500000000000011655171140017671 5ustar lukaslukaspython-musicbrainz2-0.7.4/src/musicbrainz2/wsxml.py0000644000175000017500000013371311247217206021426 0ustar lukaslukas"""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 musicbrainz2.utils as mbutils import musicbrainz2.model as model from 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 python-musicbrainz2-0.7.4/src/musicbrainz2/disc.py0000644000175000017500000001532211243756477021207 0ustar lukaslukas"""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 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 python-musicbrainz2-0.7.4/src/musicbrainz2/__init__.py0000644000175000017500000000147311655170705022015 0ustar lukaslukas"""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 13329 2011-11-05 08:20:21Z luks $' __version__ = '0.7.4' # EOF python-musicbrainz2-0.7.4/src/musicbrainz2/data/0000755000175000017500000000000011655171137020610 5ustar lukaslukaspython-musicbrainz2-0.7.4/src/musicbrainz2/data/releasetypenames.py0000644000175000017500000000207710541352746024536 0ustar lukaslukas# -*- 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 python-musicbrainz2-0.7.4/src/musicbrainz2/data/countrynames.py0000644000175000017500000001366610425115667023725 0ustar lukaslukas# -*- 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 python-musicbrainz2-0.7.4/src/musicbrainz2/data/__init__.py0000644000175000017500000000047710425115667022731 0ustar lukaslukas"""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 python-musicbrainz2-0.7.4/src/musicbrainz2/data/languagenames.py0000644000175000017500000002153210541343413023763 0ustar lukaslukas# -*- 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 python-musicbrainz2-0.7.4/src/musicbrainz2/data/scriptnames.py0000644000175000017500000000255710425115667023523 0ustar lukaslukas# -*- 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 python-musicbrainz2-0.7.4/src/musicbrainz2/webservice.py0000644000175000017500000014151111654514634022414 0ustar lukaslukas"""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 13325 2011-11-03 13:39:40Z luks $' import urllib import urllib2 import urlparse import logging import musicbrainz2 from musicbrainz2.model import Release from musicbrainz2.wsxml import MbXmlParser, ParseError import 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, userAgent=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 @param userAgent: a string containing the user agent """ 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__)) if opener is None: self._opener = urllib2.build_opener() else: self._opener = opener if userAgent is None: self._userAgent = "python-musicbrainz/" + musicbrainz2.__version__ else: self._userAgent = userAgent.replace("-", "/") \ + " python-musicbrainz/" \ + musicbrainz2.__version__ 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): req = urllib2.Request(url) req.add_header('User-Agent', self._userAgent) 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. Even if you don't plan to submit data, setting this parameter is encouraged because it will set the user agent used to make requests if you don't supply the C{ws} parameter. @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(userAgent=clientId) 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) 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 python-musicbrainz2-0.7.4/src/musicbrainz2/model.py0000644000175000017500000020002711444132513021341 0ustar lukaslukas"""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 python-musicbrainz2-0.7.4/src/musicbrainz2/utils.py0000644000175000017500000001344111654514476021422 0ustar lukaslukas"""Various utilities to simplify common tasks. This module contains helper functions to make common tasks easier. @author: Matthias Friedrich """ __revision__ = '$Id: utils.py 13322 2011-11-03 13:38:06Z luks $' import re import urlparse __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 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 python-musicbrainz2-0.7.4/CHANGES.txt0000644000175000017500000001347511655171107016320 0ustar lukaslukasChanges in 0.7.4: * Support for setting custom User-Agent string Changes in 0.7.3: * Fixed typo in Artist.addRelease (#4076) * Fixed HTTP Digest Authentication with the NGS web service (#5905) Changes in 0.7.2: * Added support for CD stub submission (patch by Alastair Porter). Changes in 0.7.1: * Added release group support (patch by John J. Jordan, review by Luks). * Added support for user collections (patch by Alastair Porter). * Fixed libdiscid detection on OpenBSD (patch by Anton Yabchinskiy). * Export model.Rating. * Minor documentation cleanups. Changes in 0.7.0: * Added ISRC support (patch by Alastair Porter). * Added rating support (patch by Peter Schnebel). * Added a trackCount parameter to ReleaseFilter (#3781). * Changed ReleaseFilter and TrackFilter to accept URIs for artistId and releaseId (#4746). * Fixed serialization of relations in MbXmlWriter (#4394). * Fixed a crash on 64-bit systems in libdiscid bindings (Luks). * Fixed a deprecation warning: use builtin set if available (#4956). Changes in 0.6.0: * Added folksonomy tagging support (mostly by Philipp Wolfer, #3285): - Added getTag(), getTags(), and addTag() methods to model.Entity. - Added getUserTags() and submitUserTags() to webservice.Query. - Added an example script: examples/folksonomytags.py. * Relation.DIR_BOTH is no longer used since the web service doesn't know bidirectional relations. If no direction is returned, the client library maps it to Relation.DIR_NONE. Note that this is *not* entirely backwards compatible (#3034). * Added "WaxCylinder" and "PianoRoll" to ReleaseEvent (#3231). * Changed download URL to HTTP for installation via setuptools (#3202). * Warn if epydoc isn't found when installing from source (by intgr, #3124). Changes in 0.5.0: * Added label support (based on Oliver's patch in #2671): - Added the model.Label and model.LabelAlias classes. - Added a label property to model.Release. - Updated the webservice.Query class. - Added webservice.LabelFilter and webservice.LabelIncludes. - Updated wsxml.Metadata, wsxml.MbXmlParser, and wsxml.MbXmlWriter. * Added catalog number and barcode properties to model.ReleaseEvent. * Changes to IFilter: - Added a query parameter to pass in Lucene queries. - Added an offset parameter to support paging through results. * Added count and offset properties to all Result classes in wsxml. Changes in 0.4.1: * Access to a CD drive other than the default drive works again (Luks). * IWebService.post() now returns a file-like object instead of None (Luks). * Added the "Artificial" language and the "Pseudo-Release" type (Luks). Changes in 0.4.0: * DiscID calculation now uses libdiscid instead of libmusicbrainz2. * Fixed the IFilter subclasses to properly encode strings (#2148) * Added several count and offset related methods in the model package. * Added Entity.getRelationTargets() for easier relation handling. * Added two (optional) parameters for Entity.getRelations(). * Added mb-submit-disc, a tool for submitting DiscIDs to MusicBrainz. Changes in 0.3.2: * Made the package work inside a zip file for py2exe (#1411, Luks). * Added the mb-submit-disc tool. Changes in 0.3.1: * Fixed webservice.WebService.post(), to make PUID submission work (Luks). * Added and updated package metadata in setup.py. Changes in 0.3.0: * API changes in model.Query to support relevance scores (#1175): - Changed getArtists() to return a list of ArtistResult objects. - Changed getReleases() to return a list of ReleaseResult objects. - Changed getTracks() to return a list of TrackResult objects. * API changes in model.ReleaseEvent: - Renamed getCountryId() to getCountry() - Renamed __init__()'s 'countryId' parameter to 'country'. * API changes in model.Artist: - Renamed setType()'s 'typeUri' parameter to 'type_'. * API changes in model.ArtistAlias: - Renamed __init__()'s and setType()'s 'typeUri' parameter to 'type_'. * API changes to wsxml.Metadata: - Removed getArtistList(), getReleaseList(), getTrackList() - Added getArtistResults(), getReleaseResults(), and getTrackResults(), returning ArtistResult, ReleaseResult, and TrackResult objects, respectively. * General API changes: - Classes are now derived from 'object' (new-style classes). - Marked all supposedly private attributes as private ('attr' -> '_attr'). - Marked the DOM utility functions in wsxml.py as private. * All getter and setter methods are now exposed as properties, too. * Added the class wsxml.MbXmlWriter which generates MMD XML. * Fixed model.Release.TYPE_EP ('Ep' -> 'EP'). * Added TYPE_PERSON and TYPE_GROUP constans to model.Artist. * Added functions to the utils module to map country, language, script and release type IDs to printable strings. * Modules now have __all__, which makes 'import *' possible (but don't do it). * The country code in model.ReleaseEvent is now optional. This is due to the server which doesn't provide country codes for release collection queries (and just the earliest date). * Added model.Release.getEarliestReleaseEvent() (#1260). * Added more example code and improved the documentation. Changes in 0.2.2: * Added the musicbrainz2.utils module (#1192). Changes in 0.2.1: * Release date parsing has been fixed (#1181). * ctypes 0.9.9.3 is now supported. * Authentication now works even after HTTP redirects (#1166). * Added type and script support to ArtistAlias. Changes in 0.2.0: * API change: Switched from TRM to PUID (Luks). All interfaces with 'Trm' or 'trm' in the name are affected. * python 2.3 compatibility fixes (#1160) Changes in 0.1.0: * Initial release. -- $Id: CHANGES.txt 13330 2011-11-05 08:22:31Z luks $ python-musicbrainz2-0.7.4/README.txt0000644000175000017500000000224211655170567016204 0ustar lukaslukasPython Bindings for the MusicBrainz XML Web Service --------------------------------------------------- This python package contains various modules for accessing the MusicBrainz web service, as well as parsing the MusicBrainz Metadata XML (MMD), or calculating DiscIDs from Audio CDs. Except for the DiscID generation, everything should work with standard python 2.3 or later. However, for DiscID calculation, libdiscid and the ctypes package are required. See the installation instructions for details. To get started quickly have a look at the examples directory which contains various sample scripts. API documentation can be generated using epydoc (http://epydoc.sourceforge.net). Please report all bugs you find via the MusicBrainz bug tracker: http://tickets.musicbrainz.org/ Questions about this package may be posted to the MusicBrainz development mailing list (mb-devel): http://musicbrainz.org/list.html More information can be found at the package's official homepage. It also contains an FAQ section which could be of help in case you run into problems: http://musicbrainz.org/products/python-musicbrainz2/ -- $Id: README.txt 13328 2011-11-05 08:19:03Z luks $ python-musicbrainz2-0.7.4/test/0000755000175000017500000000000011655171135015455 5ustar lukaslukaspython-musicbrainz2-0.7.4/test/test_wsxml_releasegroup.py0000644000175000017500000000756711231304732023023 0ustar lukaslukas"""Tests for parsing release groups using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, MbXmlWriter, ParseError from musicbrainz2.model import ReleaseGroup, NS_MMD_1 from StringIO import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') # Only have tests for valid data, ATM. RELEASEGROUP_DIR = os.path.join(VALID_DATA_DIR, 'release-group') def makeId(relativeUri): return 'http://musicbrainz.org/release-group/' + relativeUri class ParseReleaseGroupTest(unittest.TestCase): def testReleaseGroupBasic(self): f = os.path.join(RELEASEGROUP_DIR, 'The_Cure_1.xml') md = MbXmlParser().parse(f) releaseGroup = md.getReleaseGroup() self.failIf(releaseGroup is None) self.assertEquals(releaseGroup.getId(), makeId('c6a62b78-70f7-44f7-b159-064f6b7ba03a')) self.assertEquals(releaseGroup.getTitle(), 'The Cure') self.assertEquals(releaseGroup.getType(), NS_MMD_1 + 'Album') def testReleaseGroupFull(self): f = os.path.join(RELEASEGROUP_DIR, 'The_Cure_1.xml') md = MbXmlParser().parse(f) releaseGroup = md.getReleaseGroup() self.failIf(releaseGroup is None) releases = releaseGroup.getReleases() self.failIf(releases is None) self.assertEquals(len(releases), 4) # Check releases, which are in no particular order. expectedIds = [ 'd984e1a3-7281-46bb-ad8b-1478a00f2fbf', 'c100a398-3132-48a8-a5fc-c3e908ac17dc', '24bec892-b21d-47d8-a288-dc6450152574', '61a4ec51-fa34-4757-85d7-83231776ed14'] actualIds = [release.id[-36:] for release in releases] for expectedId in expectedIds: self.assert_(expectedId in actualIds) # Check artist self.assertEquals(releaseGroup.getArtist().getName(), 'The Cure') self.assertEquals(releaseGroup.getArtist().id[-36:], '69ee3720-a7cb-4402-b48d-a02c366f2bcf') def testSearchResults(self): f = os.path.join(RELEASEGROUP_DIR, 'search_result_1.xml') md = MbXmlParser().parse(f) releaseGroups = md.getReleaseGroupResults() self.failIf(releaseGroups is None) self.assertEquals(md.getReleaseGroupResultsOffset(), 0) self.assertEquals(md.getReleaseGroupResultsCount(), 3) expectedEntries = { '963eac15-e3da-3a92-aa5c-2ec23bfb6ec2': ['Signal Morning', 100], '0bd324a3-1c90-3bdb-8ca4-4101a580c62c': ['Circulatory System', 98], 'ea7d8352-7751-30be-8490-bb6df737f47c': ['Inside Views', 90]} for result in releaseGroups: releaseGroup = result.releaseGroup self.failIf(releaseGroup is None) releaseGroupId = releaseGroup.id[-36:] self.assert_(releaseGroupId in expectedEntries) expectedTitle, expectedScore = expectedEntries[releaseGroupId] self.assertEquals(releaseGroup.title, expectedTitle) self.assertEquals(result.score, expectedScore) del expectedEntries[releaseGroupId] def testSerialize(self): # Get initial. f = os.path.join(RELEASEGROUP_DIR, 'The_Cure_1.xml') md1 = MbXmlParser().parse(f) # Serialize. outbuffer = StringIO() MbXmlWriter().write(outbuffer, md1) # Deserialize. inbuffer = StringIO(outbuffer.getvalue().encode('utf-8')) md2 = MbXmlParser().parse(inbuffer) # Check releaseGroup = md2.getReleaseGroup() self.failIf(releaseGroup is None) self.assertEquals(len(releaseGroup.getReleases()), 4) self.assertEquals(releaseGroup.getId(), makeId('c6a62b78-70f7-44f7-b159-064f6b7ba03a')) self.assertEquals(releaseGroup.getTitle(), 'The Cure') self.assertEquals(releaseGroup.getType(), NS_MMD_1 + 'Album') def testEmptyType(self): f = os.path.join(RELEASEGROUP_DIR, 'The_Cure_1.xml') md1 = MbXmlParser().parse(f) releaseGroup1 = md1.getReleaseGroup() releaseGroup1.setType(None) # Serialize. outbuffer = StringIO() MbXmlWriter().write(outbuffer, md1) inbuffer = StringIO(outbuffer.getvalue().encode('utf-8')) md2 = MbXmlParser().parse(inbuffer) # Check releaseGroup2 = md2.getReleaseGroup() self.failIf(releaseGroup2 is None) self.assertEquals(releaseGroup2.getType(), None) python-musicbrainz2-0.7.4/test/__init__.py0000644000175000017500000000006510367417216017571 0ustar lukaslukas"""This package contains modules with unit tests.""" python-musicbrainz2-0.7.4/test/test_model.py0000644000175000017500000000623111444132513020161 0ustar lukaslukas"""Tests for various model classes.""" import unittest from musicbrainz2.model import Artist, Release, Track, Relation, Tag, NS_REL_1 class MiscModelTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testAddRelation(self): rel = Relation(NS_REL_1+'Producer', Relation.TO_RELEASE, 'a_id', attributes=[NS_REL_1+'Co']) artist = Artist('ar_id', 'Tori Amos', 'Person') artist.addRelation(rel) rel2 = artist.getRelations(Relation.TO_RELEASE)[0] self.assertEquals(rel.getType(), rel2.getType()) self.assertEquals(rel.getTargetType(), rel2.getTargetType()) self.assertEquals(rel.getTargetId(), rel2.getTargetId()) self.assertEquals(rel.getAttributes(), rel2.getAttributes()) self.assertEquals(rel.getBeginDate(), rel2.getBeginDate()) self.assertEquals(rel.getEndDate(), rel2.getEndDate()) self.assertEquals(artist.getRelationTargetTypes(), [ Relation.TO_RELEASE ]) # works because we only have one relation self.assertEquals(artist.getRelations(), artist.getRelations(Relation.TO_RELEASE)) rel3 = artist.getRelations(Relation.TO_RELEASE, NS_REL_1 + 'Producer') self.assertEquals(len(rel3), 1) rel4 = artist.getRelations(Relation.TO_RELEASE, NS_REL_1 + 'Producer', [NS_REL_1 + 'Co']) self.assertEquals(len(rel4), 1) rel5 = artist.getRelations(Relation.TO_RELEASE, NS_REL_1 + 'Producer', [NS_REL_1 + 'NotThere']) self.assertEquals(len(rel5), 0) rel6 = artist.getRelations(Relation.TO_RELEASE, NS_REL_1 + 'Producer', [NS_REL_1 + 'Co'], 'none') self.assertEquals(len(rel6), 1) rel6 = artist.getRelations(Relation.TO_RELEASE, NS_REL_1 + 'Producer', [NS_REL_1 + 'Co'], 'forward') self.assertEquals(len(rel6), 0) def testTrackDuration(self): t = Track() self.assert_( t.getDuration() is None ) self.assert_( t.getDurationSplit() == (0, 0) ) t.setDuration(0) self.assert_( t.getDurationSplit() == (0, 0) ) t.setDuration(218666) self.assert_( t.getDurationSplit() == (3, 39) ) def testReleaseIsSingleArtist(self): r = Release() r.setArtist(Artist(id_=1)) r.addTrack(Track()) r.addTrack(Track()) self.assert_( r.isSingleArtistRelease() ) r.getTracks()[0].setArtist(Artist(id_=7)) r.getTracks()[1].setArtist(Artist(id_=8)) self.assert_( r.isSingleArtistRelease() == False ) r.getTracks()[0].setArtist(Artist(id_=1)) r.getTracks()[1].setArtist(Artist(id_=1)) self.assert_( r.isSingleArtistRelease() ) def testTags(self): a = Artist() a.addTag(Tag('foo', 1)) a.addTag(Tag('bar', 2)) a.addTag(Tag('bar', 5)) self.assertEquals(len(a.tags), 2) self.assertEquals(a.getTag('foo').count, 1) self.assertEquals(a.getTag('bar').count, 7) class TagTest(unittest.TestCase): def test__str__(self): self.assertEquals("foo", str(Tag(u"foo"))) self.assertRaises(UnicodeEncodeError, str, Tag(u"f\u014do")) def test__unicode__(self): self.assertEquals(u"foo", unicode(Tag(u"foo"))) self.assertEquals(u"f\u014do", unicode(Tag(u"f\u014do"))) class ArtistTest(unittest.TestCase): def testAddRelease(self): release = Release() artist = Artist() artist.addRelease(release) self.assertEquals(artist.releases, [release]) # EOF python-musicbrainz2-0.7.4/test/test_first.py0000644000175000017500000000023410367417216020216 0ustar lukaslukasimport unittest class First(unittest.TestCase): def testRun(self): self.assert_( True ) def suite(): suite = unittest.makeSuite(First) return suite python-musicbrainz2-0.7.4/test/test_wsxml_user.py0000644000175000017500000000172110375126170021274 0ustar lukaslukas"""Tests for parsing user elements using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError from musicbrainz2.model import NS_MMD_1, NS_EXT_1 import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_USER_DIR = os.path.join(VALID_DATA_DIR, 'user') class ParseUserTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testUser(self): f = os.path.join(VALID_USER_DIR, 'User_1.xml') md = MbXmlParser().parse(f) userList = md.getUserList() self.assertEquals(len(userList), 1) user = userList[0] self.failIf( user is None ) self.assertEquals(user.getName(), 'matt') self.assertEquals(user.getShowNag(), False) types = user.getTypes() self.assertEquals(len(types), 2) self.assert_( NS_EXT_1 + 'AutoEditor' in types ) self.assert_( NS_EXT_1 + 'RelationshipEditor' in types ) # EOF python-musicbrainz2-0.7.4/test/test_ws_query.py0000644000175000017500000001426111347471065020753 0ustar lukaslukas"""Tests for webservice.Query.""" import unittest from musicbrainz2.model import Tag from musicbrainz2.model import Rating from musicbrainz2.model import Release from musicbrainz2.webservice import Query, IWebService, AuthenticationError, RequestError class FakeWebService(IWebService): def __init__(self): self.data = [] def post(self, entity, id_, data, version='1'): self.data.append((entity, id_, data, version)) class FakeBadAuthWebService(IWebService): def post(self, entity, id_, data, version='1'): raise AuthenticationError() class FakeBadRequestWebService(IWebService): def post(self, entity, id_, data, version='1'): raise RequestError() class QueryTest(unittest.TestCase): def testAddToUserCollection(self): ws = FakeWebService() q = Query(ws) r1 = "9e186398-9ae2-45bf-a9f6-d26bc350221e" r2 = "http://musicbrainz.org/release/6b050dcf-7ab1-456d-9e1b-c3c41c18eed2" r3 = Release("d3cc336e-1010-4252-9091-7923f0429824") q.addToUserCollection([r1, r2, r3]) self.assertEquals(len(ws.data), 1) res = ws.data[0] self.assertEquals(res[0], "collection") self.assertEquals(res[2], "add=9e186398-9ae2-45bf-a9f6-d26bc350221e%2C6b050dcf-7ab1-456d-9e1b-c3c41c18eed2%2Cd3cc336e-1010-4252-9091-7923f0429824") def testRemoveFromUserCollection(self): ws = FakeWebService() q = Query(ws) r1 = "9e186398-9ae2-45bf-a9f6-d26bc350221e" r2 = "http://musicbrainz.org/release/6b050dcf-7ab1-456d-9e1b-c3c41c18eed2" r3 = Release("d3cc336e-1010-4252-9091-7923f0429824") q.removeFromUserCollection([r1, r2, r3]) self.assertEquals(len(ws.data), 1) res = ws.data[0] self.assertEquals(res[0], "collection") self.assertEquals(res[2], "remove=9e186398-9ae2-45bf-a9f6-d26bc350221e%2C6b050dcf-7ab1-456d-9e1b-c3c41c18eed2%2Cd3cc336e-1010-4252-9091-7923f0429824") def testSubmitUserTags(self): ws = FakeWebService() q = Query(ws) t1 = [u"foo", u"bar", u"f\u014do"] t2 = [Tag(u"foo"), Tag(u"bar"), Tag(u"f\u014do")] prefix = 'http://musicbrainz.org/artist/' uri = prefix + 'c0b2500e-0cef-4130-869d-732b23ed9df5' q.submitUserTags(uri, t1) q.submitUserTags(uri, t2) self.assertEquals(len(ws.data), 2) self.assertEquals(ws.data[0], ws.data[1]) q.submitUserRating(uri, Rating(5)) q.submitUserRating(uri, 5) self.assertEquals(len(ws.data), 4) self.assertEquals(ws.data[2], ws.data[3]) def testSubmitIsrc(self): tracks2isrcs = { '6a47088b-d9e0-4088-868a-394ee3c6cd33':'NZABC0800001', 'b547acbc-58c6-4a31-9806-e2348db3a167':'NZABC0800002' } ws = FakeWebService() q = Query(ws) q.submitISRCs(tracks2isrcs) self.assertEquals(len(ws.data), 1) req = ws.data[0] qstring = 'isrc=6a47088b-d9e0-4088-868a-394ee3c6cd33+NZABC0800001&isrc=b547acbc-58c6-4a31-9806-e2348db3a167+NZABC0800002' self.assertEquals(req[0], 'track') self.assertEquals(req[2], qstring) def testSubmitIsrcBadUser(self): ws = FakeBadAuthWebService() q = Query(ws) self.assertRaises(AuthenticationError, q.submitISRCs, {}) def testSubmitIsrcBadTrack(self): ws = FakeBadRequestWebService() q = Query(ws) self.assertRaises(RequestError, q.submitISRCs, {}) def testSubmitPuid(self): tracks2puids = { '6a47088b-d9e0-4088-868a-394ee3c6cd33':'c2a2cee5-a8ca-4f89-a092-c3e1e65ab7e6', 'b547acbc-58c6-4a31-9806-e2348db3a167':'c2a2cee5-a8ca-4f89-a092-c3e1e65ab7e6' } ws = FakeWebService() q = Query(ws, clientId='test-1') q.submitPuids(tracks2puids) self.assertEquals(len(ws.data), 1) req = ws.data[0] qstring = 'client=test-1&puid=6a47088b-d9e0-4088-868a-394ee3c6cd33+c2a2cee5-a8ca-4f89-a092-c3e1e65ab7e6&puid=b547acbc-58c6-4a31-9806-e2348db3a167+c2a2cee5-a8ca-4f89-a092-c3e1e65ab7e6' self.assertEquals(req[0], 'track') self.assertEquals(req[2], qstring) def testSubmitPuidBadUser(self): ws = FakeBadAuthWebService() q = Query(ws, clientId='test-1') self.assertRaises(AuthenticationError, q.submitPuids, {}) def testSubmitPuidBadTrack(self): ws = FakeBadRequestWebService() q = Query(ws, clientId='test-1') self.assertRaises(RequestError, q.submitPuids, {}) def testSubmitPuidNoClient(self): ws = FakeWebService() q = Query(ws) self.assertRaises(AssertionError, q.submitPuids, None) def testSubmitCDStubNoClient(self): ws = FakeWebService() q = Query(ws) self.assertRaises(AssertionError, q.submitCDStub, None) def testSubmitCdStub(self): from musicbrainz2.model import Disc, CDStub ws = FakeWebService() q = Query(ws, clientId='test-1') discid = "6EmGGSLhuDYz2lNXtqrCiCCqO0o-" disc = Disc(discid) disc.firstTrackNum = 1 disc.lastTrackNum = 4 disc.sectors = 89150 disc.addTrack( (150, 20551) ) disc.addTrack( (20701, 26074) ) disc.addTrack( (46775, 19438) ) disc.addTrack( (66213, 22937) ) cdstub = CDStub(disc) cdstub.artist = "artist" cdstub.title = "title" cdstub.addTrack("trackname1") cdstub.addTrack("trackname2") cdstub.addTrack("trackname3") cdstub.addTrack("trackname4") q.submitCDStub(cdstub) cdstub.barcode = "12345" cdstub.comment = "acomment" q.submitCDStub(cdstub) self.assertEquals(len(ws.data), 2) req = ws.data[0] qstring = 'client=test-1&discid=6EmGGSLhuDYz2lNXtqrCiCCqO0o-&title=title&artist=artist&track0=trackname1&track1=trackname2&track2=trackname3&track3=trackname4&toc=1+4+89150+150+20701+46775+66213' self.assertEquals(req[0], 'release') self.assertEquals(req[2], qstring) req = ws.data[1] qstring = 'client=test-1&discid=6EmGGSLhuDYz2lNXtqrCiCCqO0o-&title=title&artist=artist&barcode=12345&comment=acomment&track0=trackname1&track1=trackname2&track2=trackname3&track3=trackname4&toc=1+4+89150+150+20701+46775+66213' self.assertEquals(req[2], qstring) cdstub._tracks = [] cdstub.addTrack("tname1", "artist1") cdstub.addTrack("tname2", "artist2") cdstub.addTrack("tname3", "artist3") cdstub.addTrack("tname4", "artist4") q.submitCDStub(cdstub) self.assertEquals(len(ws.data), 3) req = ws.data[2] qstring = 'client=test-1&discid=6EmGGSLhuDYz2lNXtqrCiCCqO0o-&title=title&artist=artist&barcode=12345&comment=acomment&track0=tname1&artist0=artist1&track1=tname2&artist1=artist2&track2=tname3&artist2=artist3&track3=tname4&artist3=artist4&toc=1+4+89150+150+20701+46775+66213' self.assertEquals(req[2], qstring) # EOF python-musicbrainz2-0.7.4/test/test_ws_filters.py0000644000175000017500000000177210550142420021242 0ustar lukaslukas"""Tests for subclasses of IFilter.""" import unittest from musicbrainz2.model import Release from musicbrainz2.webservice import ReleaseFilter class ReleaseFilterTest(unittest.TestCase): def testBasic(self): f = ReleaseFilter(title='Under the Pink') params = f.createParameters() self.assert_( ('title', 'Under the Pink') in params ) def testReleaseTypes(self): f = ReleaseFilter(artistName='Tori Amos', releaseTypes=( Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)) params = f.createParameters() self.assertEquals(len(params), 2) self.assert_( ('artist', 'Tori Amos') ) self.assert_( ('releasetypes', 'Album Official') ) def testQuery(self): try: f1 = ReleaseFilter(title='Pink', query='xyz') self.fail('title and query are mutually exclusive') except ValueError: pass except: self.fail('invalid exception') # the following shouldn't throw exceptions: f2 = ReleaseFilter(title='Pink', limit=10, offset=20) f3 = ReleaseFilter(query='Pink', limit=10, offset=20) # EOF python-musicbrainz2-0.7.4/test/test_ws_includes.py0000644000175000017500000000425311231304732021400 0ustar lukaslukas"""Tests for subclasses of IIncludes.""" import unittest from musicbrainz2.model import Release from musicbrainz2.webservice import ( ArtistIncludes, ReleaseIncludes, TrackIncludes, LabelIncludes) class ArtistIncludesTest(unittest.TestCase): def testReleases(self): inc1 = ArtistIncludes(aliases=True, releaseRelations=True) tags1 = inc1.createIncludeTags() tags1.sort() self.assertEqual(tags1, ['aliases', 'release-rels']) inc2 = ArtistIncludes(releaseRelations=True) tags2 = inc2.createIncludeTags() tags2.sort() self.assertNotEqual(tags2, ['aliases', 'release-rels']) inc3 = ArtistIncludes(aliases=True, releases=(Release.TYPE_ALBUM, Release.TYPE_OFFICIAL)) tags3 = inc3.createIncludeTags() tags3.sort() self.assertEqual(tags3, ['aliases', 'sa-Album', 'sa-Official']) inc4 = ArtistIncludes(aliases=True, vaReleases=('Bootleg',)) tags4 = inc4.createIncludeTags() tags4.sort() self.assertEqual(tags4, ['aliases', 'va-Bootleg']) inc5 = ArtistIncludes(aliases=True, vaReleases=(Release.TYPE_BOOTLEG,)) tags5 = inc5.createIncludeTags() tags5.sort() self.assertEqual(tags5, ['aliases', 'va-Bootleg']) def testReleaseGroups(self): inc = ArtistIncludes(releaseGroups=True) self.assertEqual(inc.createIncludeTags(), ['release-groups']) def testTags(self): def check(includes_class): inc1 = includes_class(tags=True) tags1 = inc1.createIncludeTags() tags1.sort() self.assertEqual(tags1, ['tags']) check(ArtistIncludes) check(ReleaseIncludes) check(TrackIncludes) check(LabelIncludes) class ReleaseIncludesTest(unittest.TestCase): # Test that including isrcs in release also pulls in tracks def testIsrcs(self): inc = ReleaseIncludes(isrcs=True) tags = inc.createIncludeTags() tags.sort() self.assertEqual(tags, ['isrcs', 'tracks']) # Test that including labels in release also pulls in release events def testReleaseEvents(self): inc = ReleaseIncludes(labels=True) tags = inc.createIncludeTags() tags.sort() self.assertEqual(tags, ['labels', 'release-events']) def testReleaseGroup(self): inc = ReleaseIncludes(releaseGroup=True) tags = inc.createIncludeTags() self.assertEqual(tags, ['release-groups']) # EOF python-musicbrainz2-0.7.4/test/test_wsxml_artist.py0000644000175000017500000001222111231304732021613 0ustar lukaslukas"""Tests for parsing artists using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError from musicbrainz2.model import NS_MMD_1 import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_ARTIST_DIR = os.path.join(VALID_DATA_DIR, 'artist') def makeId(relativeUri, resType='artist'): return 'http://musicbrainz.org/%s/%s' % (resType, relativeUri) class ParseArtistTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testArtistBasic(self): f = os.path.join(VALID_ARTIST_DIR, 'Tori_Amos_1.xml') md = MbXmlParser().parse(f) artist = md.getArtist() self.failIf( artist is None ) self.assertEquals(artist.getName(), 'Tori Amos') self.assertEquals(artist.getSortName(), 'Amos, Tori') self.assertEquals(artist.getBeginDate(), '1963-08-22') self.assertEquals(len(artist.getReleases()), 0) def testArtistFull(self): f = os.path.join(VALID_ARTIST_DIR, 'Tori_Amos_2.xml') md = MbXmlParser().parse(f) artist = md.getArtist() self.failIf( artist is None ) self.assertEquals(artist.getId(), makeId('c0b2500e-0cef-4130-869d-732b23ed9df5')) self.assertEquals(artist.getName(), 'Tori Amos') self.assertEquals(artist.getSortName(), 'Amos, Tori') self.assert_(artist.getDisambiguation() is None) self.assertEquals(artist.getUniqueName(), artist.getName()) self.assertEquals(artist.getBeginDate(), '1963-08-22') self.assertEquals(len(artist.getReleases()), 3) self.assertEquals(len(artist.getReleaseGroups()), 3) release1 = artist.getReleases()[0] self.assertEquals(release1.getTitle(), 'Strange Little Girls') self.assertEquals(release1.getAsin(), 'B00005NKYQ') # Check last release in more detail. # release3 = artist.getReleases()[2] self.assertEquals(release3.getId(), makeId('290e10c5-7efc-4f60-ba2c-0dfc0208fbf5', 'release')) self.assertEquals(len(release3.getTypes()), 2) self.assert_(NS_MMD_1 + 'Album' in release3.getTypes()) self.assert_(NS_MMD_1 + 'Official' in release3.getTypes()) self.assertEquals(release3.getTitle(), 'Under the Pink') self.assertEquals(release3.getAsin(), 'B000002IXU') self.assertEquals(release3.getArtist().getId(), makeId('c0b2500e-0cef-4130-869d-732b23ed9df5')) self.failIf(release3.getReleaseGroup() is None) self.assertEquals(release3.getReleaseGroup().id[-36:], 'ef2b891f-ca73-3e14-b38b-a68699dab8c4') events = release3.getReleaseEvents() self.assertEquals(len(events), 5) self.assertEquals(events[0].getCountry(), 'DE') self.assertEquals(events[0].getDate(), '1994-01-28') self.assertEquals(events[4].getCountry(), 'AU') self.assertEquals(events[4].getDate(), '1994-11') self.assertEquals(release3.getEarliestReleaseDate(), '1994-01-28') #self.assertEquals(release3.getDiscCount(), 4) def testAliases(self): f = os.path.join(VALID_ARTIST_DIR, 'Tori_Amos_4.xml') md = MbXmlParser().parse(f) artist = md.getArtist() self.failIf( artist is None ) self.assertEquals(artist.getDisambiguation(), 'yes, that one') self.assertEquals(artist.getName(), 'Tori Amos') self.assertEquals(artist.getUniqueName(), 'Tori Amos (yes, that one)') aliases = artist.getAliases() self.assertEquals(len(aliases), 3) self.assertEquals(aliases[0].getValue(), 'Myra Ellen Amos') self.assertEquals(aliases[0].getScript(), 'Latn') self.assertEquals(aliases[1].getValue(), 'Myra Amos') self.assertEquals(aliases[2].getValue(), 'Torie Amos') self.assertEquals(aliases[2].getType(), NS_MMD_1 + 'Misspelling') def testTags(self): f = os.path.join(VALID_ARTIST_DIR, 'Tchaikovsky-2.xml') md = MbXmlParser().parse(f) artist = md.getArtist() self.failIf( artist is None ) self.assertEquals(artist.getTag('classical').count, 100) self.assertEquals(artist.getTag('russian').count, 60) self.assertEquals(artist.getTag('romantic era').count, 40) self.assertEquals(artist.getTag('composer').count, 120) def testReleaseGroups(self): f = os.path.join(VALID_ARTIST_DIR, 'Tori_Amos_2.xml') md = MbXmlParser().parse(f) artist = md.getArtist() self.failIf(artist is None) releaseGroups = artist.getReleaseGroups() self.failIf(releaseGroups is None) self.assertEquals(len(releaseGroups), 3) expectedEntries = { 'ef2b891f-ca73-3e14-b38b-a68699dab8c4': 'Under the Pink', '1fd43909-8056-3805-b2f9-c663ce7e71e6': 'To Venus and Back', 'a69a1574-dfe3-3e2a-b499-d26d5e916041': 'Strange Little Girls'} for releaseGroup in releaseGroups: self.assertEquals(releaseGroup.getType(), NS_MMD_1 + 'Album') releaseGroupId = releaseGroup.id[-36:] self.assert_(releaseGroupId in expectedEntries) self.assertEquals(releaseGroup.getTitle(), expectedEntries[releaseGroupId]) del expectedEntries[releaseGroupId] def testSearchResults(self): f = os.path.join(VALID_ARTIST_DIR, 'search_result_1.xml') md = MbXmlParser().parse(f) self.assertEquals(md.artistResultsOffset, 0) self.assertEquals(md.artistResultsCount, 47) results = md.artistResults self.assertEquals(len(results), 3) self.assertEquals(results[0].score, 100) artist1 = results[0].artist self.assertEquals(artist1.name, 'Tori Amos') # EOF python-musicbrainz2-0.7.4/test/test_utils.py0000644000175000017500000000546710762216154020241 0ustar lukaslukas"""Tests for the utils module.""" import unittest import musicbrainz2.model as m import musicbrainz2.utils as u class UtilsTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testExtractUuid(self): artistPrefix = 'http://musicbrainz.org/artist/' uuid = 'c0b2500e-0cef-4130-869d-732b23ed9df5' mbid = artistPrefix + uuid self.assertEquals(u.extractUuid(None), None) self.assertEquals(u.extractUuid(uuid), uuid) self.assertEquals(u.extractUuid(mbid), uuid) self.assertEquals(u.extractUuid(mbid, 'artist'), uuid) # not correct, but not enough data to catch this self.assertEquals(u.extractUuid(uuid, 'release'), uuid) self.assertRaises(ValueError, u.extractUuid, mbid, 'release') self.assertRaises(ValueError, u.extractUuid, mbid, 'track') self.assertRaises(ValueError, u.extractUuid, mbid+'/xy', 'artist') invalidId = 'http://example.invalid/' + uuid self.assertRaises(ValueError, u.extractUuid, invalidId) def testExtractFragment(self): fragment = 'Album' uri = m.NS_MMD_1 + fragment self.assertEquals(u.extractFragment(None), None) self.assertEquals(u.extractFragment(fragment), fragment) self.assertEquals(u.extractFragment(uri), fragment) self.assertEquals(u.extractFragment(uri, m.NS_MMD_1), fragment) prefix = 'http://example.invalid/' self.assertRaises(ValueError, u.extractFragment, uri, prefix) def testExtractEntityType(self): prefix = 'http://musicbrainz.org' uuid = 'c0b2500e-0cef-4130-869d-732b23ed9df5' mbid1 = prefix + '/artist/' + uuid self.assertEquals(u.extractEntityType(mbid1), 'artist') mbid2 = prefix + '/release/' + uuid self.assertEquals(u.extractEntityType(mbid2), 'release') mbid3 = prefix + '/track/' + uuid self.assertEquals(u.extractEntityType(mbid3), 'track') mbid4 = prefix + '/label/' + uuid self.assertEquals(u.extractEntityType(mbid4), 'label') mbid5 = prefix + '/invalid/' + uuid self.assertRaises(ValueError, u.extractEntityType, mbid5) self.assertRaises(ValueError, u.extractEntityType, None) self.assertRaises(ValueError, u.extractEntityType, uuid) invalidUri = 'http://example.invalid/foo' self.assertRaises(ValueError, u.extractEntityType, invalidUri) def testGetCountryName(self): self.assertEquals(u.getCountryName('DE'), 'Germany') self.assertEquals(u.getCountryName('FR'), 'France') def testGetLanguageName(self): self.assertEquals(u.getLanguageName('DEU'), 'German') self.assertEquals(u.getLanguageName('ENG'), 'English') def testGetScriptName(self): self.assertEquals(u.getScriptName('Latn'), 'Latin') self.assertEquals(u.getScriptName('Cyrl'), 'Cyrillic') def testGetReleaseTypeName(self): self.assertEquals(u.getReleaseTypeName(m.Release.TYPE_ALBUM), 'Album') self.assertEquals(u.getReleaseTypeName(m.Release.TYPE_COMPILATION), 'Compilation') # EOF python-musicbrainz2-0.7.4/test/test_wsxml_release.py0000644000175000017500000001321611231304732021732 0ustar lukaslukas"""Tests for parsing releases using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError from musicbrainz2.model import VARIOUS_ARTISTS_ID, NS_MMD_1, \ Relation, ReleaseEvent import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_RELEASE_DIR = os.path.join(VALID_DATA_DIR, 'release') def makeId(relativeUri): return 'http://musicbrainz.org/release/' + relativeUri class ParseReleaseTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testReleaseBasic(self): f = os.path.join(VALID_RELEASE_DIR, 'Little_Earthquakes_1.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getId(), makeId('02232360-337e-4a3f-ad20-6cdd4c34288c')) self.assertEquals(release.getTitle(), 'Little Earthquakes') self.assertEquals(release.getTextLanguage(), 'ENG') self.assertEquals(release.getTextScript(), 'Latn') self.assertEquals(len(release.getTypes()), 2) self.assert_(NS_MMD_1 + 'Album' in release.getTypes()) self.assert_(NS_MMD_1 + 'Official' in release.getTypes()) def testReleaseFull(self): f = os.path.join(VALID_RELEASE_DIR, 'Little_Earthquakes_2.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getId(), makeId('02232360-337e-4a3f-ad20-6cdd4c34288c')) self.assertEquals(release.getArtist().getName(), 'Tori Amos') events = release.getReleaseEventsAsDict() self.assertEquals(len(events), 3) self.assertEquals(events['GB'], '1992-01-13') self.assertEquals(events['DE'], '1992-01-17') self.assertEquals(events['US'], '1992-02-25') date = release.getEarliestReleaseDate() self.assertEquals(date, '1992-01-13') event = release.getEarliestReleaseEvent() self.assertEquals(event.date, date) self.assertEquals(event.country, 'GB') discs = release.getDiscs() self.assertEquals(len(discs), 3) self.assertEquals(discs[0].getId(), 'ILKp3.bZmvoMO7wSrq1cw7WatfA-') self.assertEquals(discs[1].getId(), 'ejdrdtX1ZyvCb0g6vfJejVaLIK8-') self.assertEquals(discs[2].getId(), 'Y96eDQZbF4Z26Y5.Sxdbh3wGypo-') tracks = release.getTracks() self.assertEquals(len(tracks), 12) self.assertEquals(tracks[0].getTitle(), 'Crucify') self.assertEquals(tracks[4].getTitle(), 'Winter') def testReleaseRelations(self): f = os.path.join(VALID_RELEASE_DIR, 'Highway_61_Revisited_1.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getId(), makeId('d61a2bd9-81ac-4023-bd22-1c884d4a176c')) (rel1, rel2) = release.getRelations(Relation.TO_URL) self.assertEquals(rel1.getTargetId(), 'http://en.wikipedia.org/wiki/Highway_61_Revisited') self.assertEquals(rel1.getDirection(), Relation.DIR_NONE) self.assertEquals(rel2.getTargetId(), 'http://www.amazon.com/gp/product/B0000024SI') def testVariousArtistsRelease(self): f = os.path.join(VALID_RELEASE_DIR, 'Mission_Impossible_2.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) artistId = release.getArtist().getId() self.assertEquals(artistId, VARIOUS_ARTISTS_ID) events = release.getReleaseEventsAsDict() self.assertEquals(len(events), 1) self.assertEquals(events['EU'], '2000') track14 = release.getTracks()[14] self.assertEquals(track14.getTitle(), 'Carnival') self.assertEquals(track14.getArtist().getName(), 'Tori Amos') def testIncompleteReleaseEvent(self): f = os.path.join(VALID_RELEASE_DIR, 'Under_the_Pink_1.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getTitle(), 'Under the Pink') events = release.getReleaseEvents() self.assertEquals(len(events), 1) self.assertEquals(events[0].getDate(), '1994-01-28') def testReleaseEvents(self): f = os.path.join(VALID_RELEASE_DIR, 'Under_the_Pink_3.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getTitle(), 'Under the Pink') events = release.getReleaseEvents() self.assertEquals(len(events), 1) e1 = events[0] self.assertEquals(e1.date, '1994-01-31') self.assertEquals(e1.catalogNumber, '82567-2') self.assertEquals(e1.barcode, '07567825672') self.assertEquals(e1.format, ReleaseEvent.FORMAT_CD) self.failIf( e1.label is None ) self.assertEquals(e1.label.name, 'Atlantic Records') def testReleaseGroup(self): f = os.path.join(VALID_RELEASE_DIR, 'Under_the_Pink_2.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf(release is None) self.assertEquals(release.getTitle(), 'Under the Pink') releaseGroup = release.getReleaseGroup() self.failIf(releaseGroup is None) self.assertEquals(releaseGroup.id[-36:], 'ef2b891f-ca73-3e14-b38b-a68699dab8c4') self.assertEquals(releaseGroup.getTitle(), 'Under the Pink') self.assertEquals(releaseGroup.getType(), NS_MMD_1 + 'Album') def testTags(self): f = os.path.join(VALID_RELEASE_DIR, 'Highway_61_Revisited_2.xml') md = MbXmlParser().parse(f) release = md.getRelease() self.failIf( release is None ) self.assertEquals(release.getTag('rock').count, 100) self.assertEquals(release.getTag('blues rock').count, 40) self.assertEquals(release.getTag('folk rock').count, 40) self.assertEquals(release.getTag('dylan').count, 4) def testResultAttributes(self): f = os.path.join(VALID_RELEASE_DIR, 'search_result_1.xml') md = MbXmlParser().parse(f) self.assertEquals(md.releaseResultsOffset, 0) self.assertEquals(md.releaseResultsCount, 234) # EOF python-musicbrainz2-0.7.4/test/test_wsxml_track.py0000644000175000017500000000655011215137423021424 0ustar lukaslukas"""Tests for parsing tracks using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError from musicbrainz2.model import NS_MMD_1, NS_REL_1, Relation import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_TRACK_DIR = os.path.join(VALID_DATA_DIR, 'track') def makeId(relativeUri, resType='track'): return 'http://musicbrainz.org/%s/%s' % (resType, relativeUri) class ParseTrackTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testTrackBasic(self): f = os.path.join(VALID_TRACK_DIR, 'Silent_All_These_Years_1.xml') md = MbXmlParser().parse(f) track = md.getTrack() self.failIf( track is None ) self.assertEquals(track.getTitle(), 'Silent All These Years') self.assertEquals(track.getDuration(), 253466) def testTrackRelations(self): f = os.path.join(VALID_TRACK_DIR, 'Silent_All_These_Years_2.xml') md = MbXmlParser().parse(f) track = md.getTrack() self.failIf( track is None ) self.assertEquals(track.getTitle(), 'Silent All These Years') self.assertEquals(track.getDuration(), 253466) trackRels = track.getRelations(Relation.TO_TRACK) self.assertEquals(len(trackRels), 1) rel1 = trackRels[0] self.assertEquals(rel1.getType(), NS_REL_1 + 'Cover') self.assertEquals(rel1.getDirection(), Relation.DIR_BACKWARD) self.assertEquals(rel1.getTargetId(), makeId('31e1c0c4-967f-435e-b09a-35ee079ee234', 'track')) self.assert_( rel1.getBeginDate() is None ) self.assert_( rel1.getEndDate() is None ) self.failIf( rel1.getTarget() is None ) self.assertEquals(rel1.getTarget().getId(), makeId('31e1c0c4-967f-435e-b09a-35ee079ee234')) self.assertEquals(rel1.getTarget().getArtist().getId(), makeId('5bcd4eaa-fae7-465f-9f03-d005b959ed02', 'artist')) def testTrackFull(self): f = os.path.join(VALID_TRACK_DIR, 'Silent_All_These_Years_4.xml') md = MbXmlParser().parse(f) track = md.getTrack() self.failIf( track is None ) self.assertEquals(track.getTitle(), 'Silent All These Years') self.assertEquals(track.getDuration(), 253466) artist = track.getArtist() self.failIf( artist is None ) self.assertEquals(artist.getId(), makeId('c0b2500e-0cef-4130-869d-732b23ed9df5', 'artist')) self.assertEquals(artist.getType(), NS_MMD_1 + 'Person') self.assertEquals(artist.getName(), 'Tori Amos') self.assertEquals(artist.getSortName(), 'Amos, Tori') puids = track.getPuids() self.assertEquals(len(puids), 7) self.assertEquals(puids[0], 'c2a2cee5-a8ca-4f89-a092-c3e1e65ab7e6') self.assertEquals(puids[6], '42ab76ea-5d42-4259-85d7-e7f2c69e4485') isrcs = track.isrcs self.assertEquals(len(isrcs), 1) self.assertEquals(isrcs[0], 'USPR37300012') releases = track.getReleases() self.assertEquals(len(releases), 1) self.assertEquals(releases[0].getTitle(), 'Little Earthquakes') self.assertEquals(releases[0].getTracksOffset(), 2) def testSearchResults(self): f = os.path.join(VALID_TRACK_DIR, 'search_result_1.xml') md = MbXmlParser().parse(f) self.assertEquals(md.trackResultsOffset, 7) self.assertEquals(md.trackResultsCount, 100) results = md.getTrackResults() self.assertEquals(len(results), 3) self.assertEquals(results[0].getScore(), 100) track1 = results[0].getTrack() self.assertEquals(track1.getTitle(), 'Little Earthquakes') # EOF python-musicbrainz2-0.7.4/test/test_wsxml.py0000644000175000017500000000342010411775001020226 0ustar lukaslukas"""Tests for the MbXmlParser class.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError, _makeAbsoluteUri import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_ARTIST_DIR = os.path.join(VALID_DATA_DIR, 'artist') INVALID_ARTIST_DIR = os.path.join(INVALID_DATA_DIR, 'artist') class BaseParserTest(unittest.TestCase): """Test the most basic cases: empty and invalid documents.""" def __init__(self, name): unittest.TestCase.__init__(self, name) def testEmptyValid(self): tests = ('empty_1.xml', 'empty_2.xml') for f in self._makeFiles(VALID_ARTIST_DIR, tests): try: MbXmlParser().parse(f) except ParseError, e: self.fail(f.name + ' ' + e.msg) def testEmptyInvalid(self): tests = ('empty_1.xml', 'empty_2.xml', 'empty_3.xml') for f in self._makeFiles(INVALID_ARTIST_DIR, tests): p = MbXmlParser() try: MbXmlParser().parse(f) except ParseError: pass else: self.fail(f.name + ' ' + f.name) def testMakeAbsoluteUri(self): self.assert_(_makeAbsoluteUri('http://mb.org/artist/', None) is None) self.assertEquals('http://mb.org/artist/some_id', _makeAbsoluteUri('http://mb.org/artist/', 'some_id')) self.assertEquals('http://mb.org/artist/some_id', _makeAbsoluteUri('http://mb.org/artist/', 'http://mb.org/artist/some_id')) self.assertEquals('http://mb.org/ns/mmd-1.0#name', _makeAbsoluteUri('http://mb.org/ns/mmd-1.0#', 'name')) self.assertEquals('http://mb.org/ns/mmd-1.0#name', _makeAbsoluteUri('http://mb.org/ns/mmd-1.0#', 'http://mb.org/ns/mmd-1.0#name')) def _makeFiles(self, dir, basenames): files = [ ] for b in basenames: files.append( file(os.path.join(dir, b)) ) return files # EOF python-musicbrainz2-0.7.4/test/test_wsxml_label.py0000644000175000017500000000611010761050360021366 0ustar lukaslukas"""Tests for parsing artists using MbXmlParser.""" import unittest from musicbrainz2.wsxml import MbXmlParser, ParseError from musicbrainz2.model import NS_MMD_1 import StringIO import os.path VALID_DATA_DIR = os.path.join('test-data', 'valid') INVALID_DATA_DIR = os.path.join('test-data', 'invalid') VALID_LABEL_DIR = os.path.join(VALID_DATA_DIR, 'label') def makeId(relativeUri, resType='label'): return 'http://musicbrainz.org/%s/%s' % (resType, relativeUri) class ParseLabelTest(unittest.TestCase): def __init__(self, name): unittest.TestCase.__init__(self, name) def testLabelBasic(self): f = os.path.join(VALID_LABEL_DIR, 'Atlantic_Records_1.xml') md = MbXmlParser().parse(f) label = md.getLabel() self.failIf( label is None ) self.assertEquals(label.id, makeId('50c384a2-0b44-401b-b893-8181173339c7')) self.assertEquals(label.type, NS_MMD_1 + 'OriginalProduction') self.assertEquals(label.name, 'Atlantic Records') self.assertEquals(label.beginDate, '1947') self.assertEquals(label.endDate, None) self.assertEquals(label.country, 'US') self.assertEquals(label.code, '121') def testIncomplete(self): f = os.path.join(VALID_LABEL_DIR, 'Atlantic_Records_3.xml') md = MbXmlParser().parse(f) label = md.getLabel() self.failIf( label is None ) self.assertEquals(label.id, makeId('50c384a2-0b44-401b-b893-8181173339c7')) self.assertEquals(label.code, None) def testLabelSubElements(self): f = os.path.join(VALID_LABEL_DIR, 'Atlantic_Records_2.xml') md = MbXmlParser().parse(f) label = md.getLabel() self.failIf( label is None ) self.assertEquals(label.type, NS_MMD_1 + 'Distributor') self.assertEquals(label.name, 'Atlantic Records') self.assertEquals(label.sortName, 'AR SortName') self.assertEquals(label.disambiguation, 'fake') self.assertEquals(label.beginDate, '1947') self.assertEquals(label.endDate, '2047') self.assertEquals(label.country, 'US') self.assertEquals(label.code, '121') self.assertEquals(len(label.aliases), 1) alias = label.aliases[0] self.assertEquals(alias.value, 'Atlantic Rec.') self.assertEquals(label.getUniqueName(), 'Atlantic Records (fake)') def testTags(self): f = os.path.join(VALID_LABEL_DIR, 'Atlantic_Records_3.xml') md = MbXmlParser().parse(f) label = md.getLabel() self.failIf( label is None ) self.assertEquals(label.getTag('american').count, None) self.assertEquals(label.getTag('jazz').count, None) self.assertEquals(label.getTag('blues').count, None) def testSearchResults(self): f = os.path.join(VALID_LABEL_DIR, 'search_result_1.xml') md = MbXmlParser().parse(f) self.assertEquals(md.labelResultsOffset, 0) self.assertEquals(md.labelResultsCount, 2) self.assertEquals(md.getLabelResultsOffset(), 0) self.assertEquals(md.getLabelResultsCount(), 2) results = md.labelResults self.assertEquals(len(results), 2) self.assertEquals(results[0].score, 100) label1 = results[0].label self.assertEquals(label1.name, 'Atlantic Records') self.assertEquals(results[1].score, 46) label2 = results[1].label self.assertEquals(label2.name, 'DRO Atlantic') # EOF python-musicbrainz2-0.7.4/INSTALL.txt0000644000175000017500000000223211245512572016343 0ustar lukaslukasInstallation Instructions ------------------------- First of all, install the following dependencies: 1. python (tested with 2.4) Standard python 2.3 or later should work. -> http://www.python.org/ 2. libdiscid (tested with 0.1.0) Any version above 0.1.0 should do. -> http://musicbrainz.org/products/libdiscid/ 3. ctypes (tested with 0.9.6 and 0.9.9.3) Version 0.9.0 or later should work, which is included in recent linux distributions. This is required for DiscID calculation. Note that python 2.5 already comes with ctypes. -> http://starship.python.net/crew/theller/ctypes/ Installation works using python's standard distutils (on most systems, root permissions are required): python setup.py install Note that on Debian-based systems like Ubuntu, you have to add the --install-layout=deb command line switch or it won't work on python 2.6 and later. Optionally, API documentation can be generated using epydoc. This command runs epydoc (http://epydoc.sf.net) which creates a 'html' directory containing the documentation: python setup.py docs -- $Id: INSTALL.txt 12005 2009-08-27 14:17:30Z matt $ python-musicbrainz2-0.7.4/MANIFEST.in0000644000175000017500000000015110541343413016222 0ustar lukaslukasinclude *.txt recursive-include examples * recursive-include test *.py recursive-include test-data *.xml