Package musicbrainz2 :: Module wsxml

[frames] | no frames]

# Source Code for Module musicbrainz2.wsxml

       1  """A parser for the Music Metadata XML Format (MMD).
       2
       3  This module contains L{MbXmlParser}, which parses the U{Music Metadata XML
       4  Format (MMD) <http://musicbrainz.org/development/mmd/>} returned by the
       5  MusicBrainz webservice.
       6
       7  There are also DOM helper functions in this module used by the parser which
       8  probably aren't useful to users.
       9  """
      10  __revision__ = '$Id: wsxml.py 12028 2009-09-01 13:15:50Z matt $'
      11
      12  import re
      13  import logging
      14  import urlparse
      15  import xml.dom.minidom
      16  import xml.sax.saxutils as saxutils
      17  from xml.parsers.expat import ExpatError
      18  from xml.dom import DOMException
      19
      20  import musicbrainz2.utils as mbutils
      21  import musicbrainz2.model as model
      22  from musicbrainz2.model import NS_MMD_1, NS_REL_1, NS_EXT_1
      23
      24  __all__ = [
      25          'DefaultFactory', 'Metadata', 'ParseError',
      26          'MbXmlParser', 'MbXmlWriter',
      27          'AbstractResult',
      28          'ArtistResult', 'ReleaseResult', 'TrackResult', 'LabelResult',
      29          'ReleaseGroupResult'
      30  ]
      31
      32
      33 -class DefaultFactory(object):
      34          """A factory to instantiate classes from the domain model.
      35
      36          This factory may be used to create objects from L{musicbrainz2.model}.
      37          """
      38 -        def newArtist(self): return model.Artist()
      39 -        def newRelease(self): return model.Release()
      40 -        def newReleaseGroup(self): return model.ReleaseGroup()
      41 -        def newTrack(self): return model.Track()
      42 -        def newRelation(self): return model.Relation()
      43 -        def newReleaseEvent(self): return model.ReleaseEvent()
      44 -        def newDisc(self): return model.Disc()
      45 -        def newArtistAlias(self): return model.ArtistAlias()
      46 -        def newUser(self): return model.User()
      47 -        def newLabel(self): return model.Label()
      48 -        def newLabelAlias(self): return model.LabelAlias()
      49 -        def newTag(self): return model.Tag()
      50 -        def newRating(self): return model.Rating()
      51
      52
      53 -class ParseError(Exception):
      54          """Exception to be thrown if a parse error occurs.
      55
      56          The C{'msg'} attribute contains a printable error message, C{'reason'}
      57          is the lower level exception that was raised.
      58          """
      59
      60 -        def __init__(self, msg='Parse Error', reason=None):
      61                  Exception.__init__(self)
      62                  self.msg = msg
      63                  self.reason = reason
      64
      65 -        def __str__(self):
      66                  return self.msg
      67
      68
      69 -class Metadata(object):
      70          """Represents a parsed Music Metadata XML document.
      71
      72          The Music Metadata XML format is very flexible and may contain a
      73          diverse set of data (e.g. an artist, a release and a list of tracks),
      74          but usually only a small subset is used (either an artist, a release
      75          or a track, or a lists of objects from one class).
      76
      77          @see: L{MbXmlParser} for reading, and L{MbXmlWriter} for writing
      78                  Metadata objects
      79          """
      80 -        def __init__(self):
      81                  self._artist = None
      82                  self._release = None
      83                  self._track = None
      84                  self._label = None
      85                  self._releaseGroup = None
      86                  self._artistResults = [ ]
      87                  self._artistResultsOffset = None
      88                  self._artistResultsCount = None
      89                  self._releaseResults = [ ]
      90                  self._releaseResultsOffset = None
      91                  self._releaseResultsCount = None
      92                  self._releaseGroupResults = [ ]
      93                  self._releaseGroupResultsOffset = None
      94                  self._releaseGroupResultsCount = None
      95                  self._trackResults = [ ]
      96                  self._trackResultsOffset = None
      97                  self._trackResultsCount = None
      98                  self._labelResults = [ ]
      99                  self._labelResultsOffset = None
     100                  self._labelResultsCount = None
     101                  self._tagList = [ ]
     102                  self._rating = None
     103                  self._userList = [ ]
     104
     105 -        def getArtist(self):
     106                  return self._artist
     107
     108 -        def setArtist(self, artist):
     109                  self._artist = artist
     110
     111          artist = property(getArtist, setArtist, doc='An Artist object.')
     112
     113 -        def getLabel(self):
     114                  return self._label
     115
     116 -        def setLabel(self, label):
     117                  self._label = label
     118
     119          label = property(getLabel, setLabel, doc='A Label object.')
     120
     121 -        def getRelease(self):
     122                  return self._release
     123
     124 -        def setRelease(self, release):
     125                  self._release = release
     126
     127          release = property(getRelease, setRelease, doc='A Release object.')
     128
     129 -        def getReleaseGroup(self):
     130                  return self._releaseGroup
     131
     132 -        def setReleaseGroup(self, releaseGroup):
     133                  self._releaseGroup = releaseGroup
     134
     135          releaseGroup = property(getReleaseGroup, setReleaseGroup)
     136
     137 -        def getTrack(self):
     138                  return self._track
     139
     140 -        def setTrack(self, track):
     141                  self._track = track
     142
     143          track = property(getTrack, setTrack, doc='A Track object.')
     144
     145 -        def getArtistResults(self):
     146                  """Returns an artist result list.
     147
     148                  @return: a list of L{ArtistResult} objects.
     149                  """
     150                  return self._artistResults
     151
     152          artistResults = property(getArtistResults,
     153                  doc='A list of ArtistResult objects.')
     154
     155 -        def getArtistResultsOffset(self):
     156                  """Returns the offset of the artist result list.
     157
     158                  The offset is used for paging through the result list. It
     159                  is zero-based.
     160
     161                  @return: an integer containing the offset, or None
     162
     163                  @see: L{getArtistResults}, L{getArtistResultsCount}
     164                  """
     165                  return self._artistResultsOffset
     166
     167 -        def setArtistResultsOffset(self, value):
     168                  """Sets the offset of the artist result list.
     169
     170                  @param value: an integer containing the offset, or None
     171
     172                  @see: L{getArtistResultsOffset}
     173                  """
     174                  self._artistResultsOffset = value
     175
     176          artistResultsOffset = property(
     177                  getArtistResultsOffset, setArtistResultsOffset,
     178                  doc='The offset of the artist results.')
     179
     180 -        def getArtistResultsCount(self):
     181                  """Returns the total number of results available.
     182
     183                  This may or may not match with the number of elements that
     184                  L{getArtistResults} returns. If the count is higher than
     185                  the list, it indicates that the list is incomplete.
     186
     187                  @return: an integer containing the count, or None
     188
     189                  @see: L{setArtistResultsCount}, L{getArtistResultsOffset}
     190                  """
     191                  return self._artistResultsCount
     192
     193 -        def setArtistResultsCount(self, value):
     194                  """Sets the total number of available results.
     195
     196                  @param value: an integer containing the count, or None
     197
     198                  @see: L{getArtistResults}, L{setArtistResultsOffset}
     199                  """
     200                  self._artistResultsCount = value
     201
     202          artistResultsCount = property(
     203                  getArtistResultsCount, setArtistResultsCount,
     204                  doc='The total number of artists results.')
     205
     206 -        def getLabelResults(self):
     207                  """Returns a label result list.
     208
     209                  @return: a list of L{LabelResult} objects.
     210                  """
     211                  return self._labelResults
     212
     213          labelResults = property(getLabelResults,
     214                  doc='A list of LabelResult objects')
     215
     216 -        def getLabelResultsOffset(self):
     217                  """Returns the offset of the label result list.
     218
     219                  The offset is used for paging through the result list. It
     220                  is zero-based.
     221
     222                  @return: an integer containing the offset, or None
     223
     224                  @see: L{getLabelResults}, L{getLabelResultsCount}
     225                  """
     226                  return self._labelResultsOffset
     227
     228 -        def setLabelResultsOffset(self, value):
     229                  """Sets the offset of the label result list.
     230
     231                  @param value: an integer containing the offset, or None
     232
     233                  @see: L{getLabelResultsOffset}
     234                  """
     235                  self._labelResultsOffset = value
     236
     237          labelResultsOffset = property(
     238                  getLabelResultsOffset, setLabelResultsOffset,
     239                  doc='The offset of the label results.')
     240
     241 -        def getLabelResultsCount(self):
     242                  """Returns the total number of results available.
     243
     244                  This may or may not match with the number of elements that
     245                  L{getLabelResults} returns. If the count is higher than
     246                  the list, it indicates that the list is incomplete.
     247
     248                  @return: an integer containing the count, or None
     249
     250                  @see: L{setLabelResultsCount}, L{getLabelResultsOffset}
     251                  """
     252                  return self._labelResultsCount
     253
     254 -        def setLabelResultsCount(self, value):
     255                  """Sets the total number of available results.
     256
     257                  @param value: an integer containing the count, or None
     258
     259                  @see: L{getLabelResults}, L{setLabelResultsOffset}
     260                  """
     261                  self._labelResultsCount = value
     262
     263          labelResultsCount = property(
     264                  getLabelResultsCount, setLabelResultsCount,
     265                  doc='The total number of label results.')
     266
     267 -        def getReleaseResults(self):
     268                  """Returns a release result list.
     269
     270                  @return: a list of L{ReleaseResult} objects.
     271                  """
     272                  return self._releaseResults
     273
     274          releaseResults = property(getReleaseResults,
     275                  doc='A list of ReleaseResult objects.')
     276
     277 -        def getReleaseResultsOffset(self):
     278                  """Returns the offset of the release result list.
     279
     280                  The offset is used for paging through the result list. It
     281                  is zero-based.
     282
     283                  @return: an integer containing the offset, or None
     284
     285                  @see: L{getReleaseResults}, L{getReleaseResultsCount}
     286                  """
     287                  return self._releaseResultsOffset
     288
     289 -        def setReleaseResultsOffset(self, value):
     290                  """Sets the offset of the release result list.
     291
     292                  @param value: an integer containing the offset, or None
     293
     294                  @see: L{getReleaseResultsOffset}
     295                  """
     296                  self._releaseResultsOffset = value
     297
     298          releaseResultsOffset = property(
     299                  getReleaseResultsOffset, setReleaseResultsOffset,
     300                  doc='The offset of the release results.')
     301
     302 -        def getReleaseResultsCount(self):
     303                  """Returns the total number of results available.
     304
     305                  This may or may not match with the number of elements that
     306                  L{getReleaseResults} returns. If the count is higher than
     307                  the list, it indicates that the list is incomplete.
     308
     309                  @return: an integer containing the count, or None
     310
     311                  @see: L{setReleaseResultsCount}, L{getReleaseResultsOffset}
     312                  """
     313                  return self._releaseResultsCount
     314
     315 -        def setReleaseResultsCount(self, value):
     316                  """Sets the total number of available results.
     317
     318                  @param value: an integer containing the count, or None
     319
     320                  @see: L{getReleaseResults}, L{setReleaseResultsOffset}
     321                  """
     322                  self._releaseResultsCount = value
     323
     324          releaseResultsCount = property(
     325                  getReleaseResultsCount, setReleaseResultsCount,
     326                  doc='The total number of release results.')
     327
     328 -        def getReleaseGroupResults(self):
     329                  """Returns a release group result list.
     330
     331                  @return: a list of L{ReleaseGroupResult} objects.
     332                  """
     333                  return self._releaseGroupResults
     334
     335          releaseGroupResults = property(getReleaseGroupResults,
     336                  doc = 'A list of ReleaseGroupResult objects.')
     337
     338 -        def getReleaseGroupResultsOffset(self):
     339                  """Returns the offset of the release group result list.
     340
     341                  The offset is used for paging through the result list.  It
     342                  is zero-based.
     343
     344                  @return: an integer containing the offset, or None.
     345
     346                  @see: L{getReleaseGroupResults}, L{getReleaseGroupResultsCount}
     347                  """
     348                  return self._releaseGroupResultsOffset
     349
     350 -        def setReleaseGroupResultsOffset(self, value):
     351                  """Sets the offset of the release group result list.
     352
     353                  @param value: an integer containing the offset, or None
     354
     355                  @see: L{getReleaseGroupResultsOffset}
     356                  """
     357                  self._releaseGroupResultsOffset = value
     358
     359          releaseGroupResultsOffset = property(
     360                  getReleaseGroupResultsOffset, setReleaseGroupResultsOffset,
     361                  doc='The offset of the release group results.')
     362
     363 -        def getReleaseGroupResultsCount(self):
     364                  """Returns the total number of results available.
     365
     366                  This may or may not match with the number of elements that
     367                  L{getReleaseGroupResults} returns. If the count is higher than
     368                  the list, it indicates that the list is incomplete.
     369
     370                  @return: an integer containing the count, or None
     371
     372                  @see: L{setReleaseGroupResultsCount}, L{getReleaseGroupResultsOffset}
     373                  """
     374                  return self._releaseGroupResultsCount
     375
     376 -        def setReleaseGroupResultsCount(self, value):
     377                  """Sets the total number of available results.
     378
     379                  @param value: an integer containing the count, or None
     380
     381                  @see: L{getReleaseGroupResults}, L{setReleaseGroupResultsOffset}
     382                  """
     383                  self._releaseGroupResultsCount = value
     384
     385          releaseGroupResultsCount = property(
     386                  getReleaseGroupResultsCount, setReleaseGroupResultsCount,
     387                  doc='The total number of release group results.')
     388
     389 -        def getTrackResults(self):
     390                  """Returns a track result list.
     391
     392                  @return: a list of L{TrackResult} objects.
     393                  """
     394                  return self._trackResults
     395
     396          trackResults = property(getTrackResults,
     397                  doc='A list of TrackResult objects.')
     398
     399 -        def getTrackResultsOffset(self):
     400                  """Returns the offset of the track result list.
     401
     402                  The offset is used for paging through the result list. It
     403                  is zero-based.
     404
     405                  @return: an integer containing the offset, or None
     406
     407                  @see: L{getTrackResults}, L{getTrackResultsCount}
     408                  """
     409                  return self._trackResultsOffset
     410
     411 -        def setTrackResultsOffset(self, value):
     412                  """Sets the offset of the track result list.
     413
     414                  @param value: an integer containing the offset, or None
     415
     416                  @see: L{getTrackResultsOffset}
     417                  """
     418                  self._trackResultsOffset = value
     419
     420          trackResultsOffset = property(
     421                  getTrackResultsOffset, setTrackResultsOffset,
     422                  doc='The offset of the track results.')
     423
     424 -        def getTrackResultsCount(self):
     425                  """Returns the total number of results available.
     426
     427                  This may or may not match with the number of elements that
     428                  L{getTrackResults} returns. If the count is higher than
     429                  the list, it indicates that the list is incomplete.
     430
     431                  @return: an integer containing the count, or None
     432
     433                  @see: L{setTrackResultsCount}, L{getTrackResultsOffset}
     434                  """
     435                  return self._trackResultsCount
     436
     437 -        def setTrackResultsCount(self, value):
     438                  """Sets the total number of available results.
     439
     440                  @param value: an integer containing the count, or None
     441
     442                  @see: L{getTrackResults}, L{setTrackResultsOffset}
     443                  """
     444                  self._trackResultsCount = value
     445
     446          trackResultsCount = property(
     447                  getTrackResultsCount, setTrackResultsCount,
     448                  doc='The total number of track results.')
     449
     450
     451 -        def getTagList(self):
     452                  """Returns a list of tags.
     453
     454                  @return: a list of L{model.Tag} objects
     455                  """
     456                  return self._tagList
     457
     458          tagResults = property(getTagList,
     459                  doc='A list of Tag objects.')
     460
     461 -        def getRating(self):
     462                  """Returns the rating.
     463
     464                  @return: rating object
     465                  """
     466                  return self._rating
     467
     468 -        def setRating(self, value):
     469                  """Sets the rating.
     470
     471                  @param value: a L{model.Rating} object
     472                  """
     473                  self._rating = value
     474
     475          rating = property(getRating, setRating, doc='A Rating object.')
     476
     477
     478          # MusicBrainz extension to the schema
     479 -        def getUserList(self):
     480                  """Returns a list of users.
     481
     482                  @return: a list of L{model.User} objects
     483
     484                  @note: This is a MusicBrainz extension.
     485                  """
     486                  return self._userList
     487
     488          userResults = property(getUserList,
     489                  doc='A list of User objects.')
     490
     491
     492 -class AbstractResult(object):
     493          """The abstract representation of a result.
     494
     495          A result is an instance of some kind (Artist, Release, ...)
     496          associated with a score.
     497          """
     498
     499 -        def __init__(self, score):
     500                  self._score = score
     501
     502 -        def getScore(self):
     503                  """Returns the result score.
     504
     505                  The score indicates how good this result matches the search
     506                  parameters. The higher the value, the better the match.
     507
     508                  @return: an int between 0 and 100 (both inclusive), or None
     509                  """
     510                  return self._score
     511
     512 -        def setScore(self, score):
     513                  self._score = score
     514
     515          score = property(getScore, setScore, doc='The relevance score.')
     516
     517
     518 -class ArtistResult(AbstractResult):
     519          """Represents an artist result.
     520
     521          An ArtistResult consists of a I{score} and an artist. The score is a
     522          number between 0 and 100, where a higher number indicates a better
     523          match.
     524          """
     525 -        def __init__(self, artist, score):
     526                  super(ArtistResult, self).__init__(score)
     527                  self._artist = artist
     528
     529 -        def getArtist(self):
     530                  """Returns an Artist object.
     531
     532                  @return: a L{musicbrainz2.model.Artist} object
     533                  """
     534                  return self._artist
     535
     536 -        def setArtist(self, artist):
     537                  self._artist = artist
     538
     539          artist = property(getArtist, setArtist, doc='An Artist object.')
     540
     541
     542 -class ReleaseResult(AbstractResult):
     543          """Represents a release result.
     544
     545          A ReleaseResult consists of a I{score} and a release. The score is a
     546          number between 0 and 100, where a higher number indicates a better
     547          match.
     548          """
     549 -        def __init__(self, release, score):
     550                  super(ReleaseResult, self).__init__(score)
     551                  self._release = release
     552
     553 -        def getRelease(self):
     554                  """Returns a Release object.
     555
     556                  @return: a L{musicbrainz2.model.Release} object
     557                  """
     558                  return self._release
     559
     560 -        def setRelease(self, release):
     561                  self._release = release
     562
     563          release = property(getRelease, setRelease, doc='A Release object.')
     564
     565 -class ReleaseGroupResult(AbstractResult):
     566          """Represents a release group result.
     567
     568          A ReleaseGroupResult consists of a I{score} and a release group.  The
     569          score is a number between 0 and 100, where a higher number indicates
     570          a better match.
     571          """
     572 -        def __init__(self, releaseGroup, score):
     573                  super(ReleaseGroupResult, self).__init__(score)
     574                  self._releaseGroup = releaseGroup
     575
     576 -        def getReleaseGroup(self):
     577                  """Returns a ReleaseGroup object.
     578
     579                  @return: a L{musicbrainz2.model.ReleaseGroup} object
     580                  """
     581                  return self._releaseGroup
     582
     583 -        def setReleaseGroup(self, value):
     584                  self._releaseGroup = value
     585
     586          releaseGroup = property(getReleaseGroup, setReleaseGroup, doc='A ReleaseGroup object.')
     587
     588 -class TrackResult(AbstractResult):
     589          """Represents a track result.
     590
     591          A TrackResult consists of a I{score} and a track. The score is a
     592          number between 0 and 100, where a higher number indicates a better
     593          match.
     594          """
     595 -        def __init__(self, track, score):
     596                  super(TrackResult, self).__init__(score)
     597                  self._track = track
     598
     599 -        def getTrack(self):
     600                  """Returns a Track object.
     601
     602                  @return: a L{musicbrainz2.model.Track} object
     603                  """
     604                  return self._track
     605
     606 -        def setTrack(self, track):
     607                  self._track = track
     608
     609          track = property(getTrack, setTrack, doc='A Track object.')
     610
     611
     612 -class LabelResult(AbstractResult):
     613          """Represents a label result.
     614
     615          An LabelResult consists of a I{score} and a label. The score is a
     616          number between 0 and 100, where a higher number indicates a better
     617          match.
     618          """
     619 -        def __init__(self, label, score):
     620                  super(LabelResult, self).__init__(score)
     621                  self._label = label
     622
     623 -        def getLabel(self):
     624                  """Returns a Label object.
     625
     626                  @return: a L{musicbrainz2.model.Label} object
     627                  """
     628                  return self._label
     629
     630 -        def setLabel(self, label):
     631                  self._label = label
     632
     633          label = property(getLabel, setLabel, doc='A Label object.')
     634
     635
     636 -class MbXmlParser(object):
     637          """A parser for the Music Metadata XML format.
     638
     639          This parser supports all basic features and extensions defined by
     640          MusicBrainz, including unlimited document nesting. By default it
     641          reads an XML document from a file-like object (stream) and returns
     642          an object tree representing the document using classes from
     643          L{musicbrainz2.model}.
     644
     645          The implementation tries to be as permissive as possible. Invalid
     646          contents are skipped, but documents have to be well-formed and using
     647          the correct namespace. In case of unrecoverable errors, a L{ParseError}
     648          exception is raised.
     649
     650          @see: U{The Music Metadata XML Format
     651                  <http://musicbrainz.org/development/mmd/>}
     652          """
     653
     654 -        def __init__(self, factory=DefaultFactory()):
     655                  """Constructor.
     656
     657                  The C{factory} parameter has be an instance of L{DefaultFactory}
     658                  or a subclass of it. It is used by L{parse} to obtain objects
     659                  from L{musicbrainz2.model} to build resulting object tree.
     660                  If you supply your own factory, you have to make sure all
     661                  returned objects have the same interface as their counterparts
     662                  from L{musicbrainz2.model}.
     663
     664                  @param factory: an object factory
     665                  """
     666                  self._log = logging.getLogger(str(self.__class__))
     667                  self._factory = factory
     668
     669 -        def parse(self, inStream):
     670                  """Parses the MusicBrainz web service XML.
     671
     672                  Returns a L{Metadata} object representing the parsed XML or
     673                  raises a L{ParseError} exception if the data was malformed.
     674                  The parser tries to be liberal and skips invalid content if
     675                  possible.
     676
     677                  Note that an L{IOError} may be raised if there is a problem
     678                  reading C{inStream}.
     679
     680                  @param inStream: a file-like object
     681                  @return: a L{Metadata} object (never None)
     682                  @raise ParseError: if the document is not valid
     683                  @raise IOError: if reading from the stream failed
     684                  """
     685
     686                  try:
     687                          doc = xml.dom.minidom.parse(inStream)
     688
     689                          # Try to find the root element. If this isn't an mmd
     690                          # XML file or the namespace is wrong, this will fail.
     691                          elems = doc.getElementsByTagNameNS(NS_MMD_1, 'metadata')
     692
     693                          if len(elems) != 0:
     694                                  md = self._createMetadata(elems[0])
     695                          else:
     696                                  msg = 'cannot find root element mmd:metadata'
     697                                  self._log.debug('ParseError: ' + msg)
     698                                  raise ParseError(msg)
     699
     700                          doc.unlink()
     701
     702                          return md
     703                  except ExpatError, e:
     704                          self._log.debug('ExpatError: ' + str(e))
     705                          raise ParseError(msg=str(e), reason=e)
     706                  except DOMException, e:
     707                          self._log.debug('DOMException: ' + str(e))
     708                          raise ParseError(msg=str(e), reason=e)
     709
     710
     711 -        def _createMetadata(self, metadata):
     712                  md = Metadata()
     713
     714                  for node in _getChildElements(metadata):
     715                          if _matches(node, 'artist'):
     716                                  md.artist = self._createArtist(node)
     717                          elif _matches(node, 'release'):
     718                                  md.release = self._createRelease(node)
     719                          elif _matches(node, 'release-group'):
     720                                  md.releaseGroup = self._createReleaseGroup(node)
     721                          elif _matches(node, 'track'):
     722                                  md.track = self._createTrack(node)
     723                          elif _matches(node, 'label'):
     724                                  md.label = self._createLabel(node)
     725                          elif _matches(node, 'artist-list'):
     726                                  (offset, count) = self._getListAttrs(node)
     727                                  md.artistResultsOffset = offset
     728                                  md.artistResultsCount = count
     729                                  self._addArtistResults(node, md.getArtistResults())
     730                          elif _matches(node, 'release-list'):
     731                                  (offset, count) = self._getListAttrs(node)
     732                                  md.releaseResultsOffset = offset
     733                                  md.releaseResultsCount = count
     734                                  self._addReleaseResults(node, md.getReleaseResults())
     735                          elif _matches(node, 'release-group-list'):
     736                                  (offset, count) = self._getListAttrs(node)
     737                                  md.releaseGroupResultsOffset = offset
     738                                  md.releaseGroupResultsCount = count
     739                                  self._addReleaseGroupResults(node, md.getReleaseGroupResults())
     740                          elif _matches(node, 'track-list'):
     741                                  (offset, count) = self._getListAttrs(node)
     742                                  md.trackResultsOffset = offset
     743                                  md.trackResultsCount = count
     744                                  self._addTrackResults(node, md.getTrackResults())
     745                          elif _matches(node, 'label-list'):
     746                                  (offset, count) = self._getListAttrs(node)
     747                                  md.labelResultsOffset = offset
     748                                  md.labelResultsCount = count
     749                                  self._addLabelResults(node, md.getLabelResults())
     750                          elif _matches(node, 'tag-list'):
     751                                  self._addTagsToList(node, md.getTagList())
     752                          elif _matches(node, 'user-list', NS_EXT_1):
     753                                  self._addUsersToList(node, md.getUserList())
     754
     755                  return md
     756
     757
     758 -        def _addArtistResults(self, listNode, resultList):
     759                  for c in _getChildElements(listNode):
     760                          artist = self._createArtist(c)
     761                          score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1)
     762                          if artist is not None:
     763                                  resultList.append(ArtistResult(artist, score))
     764
     765 -        def _addReleaseResults(self, listNode, resultList):
     766                  for c in _getChildElements(listNode):
     767                          release = self._createRelease(c)
     768                          score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1)
     769                          if release is not None:
     770                                  resultList.append(ReleaseResult(release, score))
     771
     772 -        def _addReleaseGroupResults(self, listNode, resultList):
     773                  for c in _getChildElements(listNode):
     774                          releaseGroup = self._createReleaseGroup(c)
     775                          score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1)
     776                          if releaseGroup is not None:
     777                                  resultList.append(ReleaseGroupResult(releaseGroup, score))
     778
     779 -        def _addTrackResults(self, listNode, resultList):
     780                  for c in _getChildElements(listNode):
     781                          track = self._createTrack(c)
     782                          score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1)
     783                          if track is not None:
     784                                  resultList.append(TrackResult(track, score))
     785
     786 -        def _addLabelResults(self, listNode, resultList):
     787                  for c in _getChildElements(listNode):
     788                          label = self._createLabel(c)
     789                          score = _getIntAttr(c, 'score', 0, 100, ns=NS_EXT_1)
     790                          if label is not None:
     791                                  resultList.append(LabelResult(label, score))
     792
     793 -        def _addReleasesToList(self, listNode, resultList):
     794                  self._addToList(listNode, resultList, self._createRelease)
     795
     796 -        def _addReleaseGroupsToList(self, listNode, resultList):
     797                  self._addToList(listNode, resultList, self._createReleaseGroup)
     798
     799 -        def _addTracksToList(self, listNode, resultList):
     800                  self._addToList(listNode, resultList, self._createTrack)
     801
     802 -        def _addUsersToList(self, listNode, resultList):
     803                  self._addToList(listNode, resultList, self._createUser)
     804
     805 -        def _addTagsToList(self, listNode, resultList):
     806                  self._addToList(listNode, resultList, self._createTag)
     807
     808 -        def _addTagsToEntity(self, listNode, entity):
     809                  for node in _getChildElements(listNode):
     810                          tag = self._createTag(node)
     811                          entity.addTag(tag)
     812
     813 -        def _addRatingToEntity(self, attrNode, entity):
     814                  rating = self._createRating(attrNode)
     815                  entity.setRating(rating)
     816
     817 -        def _addToList(self, listNode, resultList, creator):
     818                  for c in _getChildElements(listNode):
     819                          resultList.append(creator(c))
     820
     821 -        def _getListAttrs(self, listNode):
     822                  offset = _getIntAttr(listNode, 'offset')
     823                  count = _getIntAttr(listNode, 'count')
     824                  return (offset, count)
     825
     826
     827 -        def _createArtist(self, artistNode):
     828                  artist = self._factory.newArtist()
     829                  artist.setId(_getIdAttr(artistNode, 'id', 'artist'))
     830                  artist.setType(_getUriAttr(artistNode, 'type'))
     831
     832                  for node in _getChildElements(artistNode):
     833                          if _matches(node, 'name'):
     834                                  artist.setName(_getText(node))
     835                          elif _matches(node, 'sort-name'):
     836                                  artist.setSortName(_getText(node))
     837                          elif _matches(node, 'disambiguation'):
     838                                  artist.setDisambiguation(_getText(node))
     839                          elif _matches(node, 'life-span'):
     840                                  artist.setBeginDate(_getDateAttr(node, 'begin'))
     841                                  artist.setEndDate(_getDateAttr(node, 'end'))
     842                          elif _matches(node, 'alias-list'):
     843                                  self._addArtistAliases(node, artist)
     844                          elif _matches(node, 'release-list'):
     845                                  (offset, count) = self._getListAttrs(node)
     846                                  artist.setReleasesOffset(offset)
     847                                  artist.setReleasesCount(count)
     848                                  self._addReleasesToList(node, artist.getReleases())
     849                          elif _matches(node, 'release-group-list'):
     850                                  (offset, count) = self._getListAttrs(node)
     851                                  artist.setReleaseGroupsOffset(offset)
     852                                  artist.setReleaseGroupsCount(count)
     853                                  self._addReleaseGroupsToList(node, artist.getReleaseGroups())
     854                          elif _matches(node, 'relation-list'):
     855                                  self._addRelationsToEntity(node, artist)
     856                          elif _matches(node, 'tag-list'):
     857                                  self._addTagsToEntity(node, artist)
     858                          elif _matches(node, 'rating'):
     859                                  self._addRatingToEntity(node, artist)
     860
     861                  return artist
     862
     863 -        def _createLabel(self, labelNode):
     864                  label = self._factory.newLabel()
     865                  label.setId(_getIdAttr(labelNode, 'id', 'label'))
     866                  label.setType(_getUriAttr(labelNode, 'type'))
     867
     868                  for node in _getChildElements(labelNode):
     869                          if _matches(node, 'name'):
     870                                  label.setName(_getText(node))
     871                          if _matches(node, 'sort-name'):
     872                                  label.setSortName(_getText(node))
     873                          elif _matches(node, 'disambiguation'):
     874                                  label.setDisambiguation(_getText(node))
     875                          elif _matches(node, 'label-code'):
     876                                  label.setCode(_getText(node))
     877                          elif _matches(node, 'country'):
     878                                  country = _getText(node, '^[A-Z]{2}$')
     879                                  label.setCountry(country)
     880                          elif _matches(node, 'life-span'):
     881                                  label.setBeginDate(_getDateAttr(node, 'begin'))
     882                                  label.setEndDate(_getDateAttr(node, 'end'))
     883                          elif _matches(node, 'alias-list'):
     884                                  self._addLabelAliases(node, label)
     885                          elif _matches(node, 'tag-list'):
     886                                  self._addTagsToEntity(node, label)
     887                          elif _matches(node, 'rating'):
     888                                  self._addRatingToEntity(node, label)
     889
     890                  return label
     891
     892 -        def _createRelease(self, releaseNode):
     893                  release = self._factory.newRelease()
     894                  release.setId(_getIdAttr(releaseNode, 'id', 'release'))
     895                  for t in _getUriListAttr(releaseNode, 'type'):
     896                          release.addType(t)
     897
     898                  for node in _getChildElements(releaseNode):
     899                          if _matches(node, 'title'):
     900                                  release.setTitle(_getText(node))
     901                          elif _matches(node, 'text-representation'):
     902                                  lang = _getAttr(node, 'language', '^[A-Z]{3}$')
     903                                  release.setTextLanguage(lang)
     904                                  script = _getAttr(node, 'script', '^[A-Z][a-z]{3}$')
     905                                  release.setTextScript(script)
     906                          elif _matches(node, 'asin'):
     907                                  release.setAsin(_getText(node))
     908                          elif _matches(node, 'artist'):
     909                                  release.setArtist(self._createArtist(node))
     910                          elif _matches(node, 'release-event-list'):
     911                                  self._addReleaseEvents(node, release)
     912                          elif _matches(node, 'release-group'):
     913                                  release.setReleaseGroup(self._createReleaseGroup(node))
     914                          elif _matches(node, 'disc-list'):
     915                                  self._addDiscs(node, release)
     916                          elif _matches(node, 'track-list'):
     917                                  (offset, count) = self._getListAttrs(node)
     918                                  release.setTracksOffset(offset)
     919                                  release.setTracksCount(count)
     920                                  self._addTracksToList(node, release.getTracks())
     921                          elif _matches(node, 'relation-list'):
     922                                  self._addRelationsToEntity(node, release)
     923                          elif _matches(node, 'tag-list'):
     924                                  self._addTagsToEntity(node, release)
     925                          elif _matches(node, 'rating'):
     926                                  self._addRatingToEntity(node, release)
     927
     928                  return release
     929
     930 -        def _createReleaseGroup(self, node):
     931                  rg = self._factory.newReleaseGroup()
     932                  rg.setId(_getIdAttr(node, 'id', 'release-group'))
     933                  rg.setType(_getUriAttr(node, 'type'))
     934
     935                  for child in _getChildElements(node):
     936                          if _matches(child, 'title'):
     937                                  rg.setTitle(_getText(child))
     938                          elif _matches(child, 'artist'):
     939                                  rg.setArtist(self._createArtist(child))
     940                          elif _matches(child, 'release-list'):
     941                                  (offset, count) = self._getListAttrs(child)
     942                                  rg.setReleasesOffset(offset)
     943                                  rg.setReleasesCount(count)
     944                                  self._addReleasesToList(child, rg.getReleases())
     945
     946                  return rg
     947
     948 -        def _addReleaseEvents(self, releaseListNode, release):
     949                  for node in _getChildElements(releaseListNode):
     950                          if _matches(node, 'event'):
     951                                  country = _getAttr(node, 'country', '^[A-Z]{2}$')
     952                                  date = _getDateAttr(node, 'date')
     953                                  catalogNumber = _getAttr(node, 'catalog-number')
     954                                  barcode = _getAttr(node, 'barcode')
     955                                  format = _getUriAttr(node, 'format')
     956
     957                                  # The date attribute is mandatory. If it isn't present,
     958                                  # we don't add anything from this release event.
     959                                  if date is not None:
     960                                          event = self._factory.newReleaseEvent()
     961                                          event.setCountry(country)
     962                                          event.setDate(date)
     963                                          event.setCatalogNumber(catalogNumber)
     964                                          event.setBarcode(barcode)
     965                                          event.setFormat(format)
     966
     967                                          for subNode in _getChildElements(node):
     968                                                  if _matches(subNode, 'label'):
     969                                                          event.setLabel(self._createLabel(subNode))
     970
     971                                          release.addReleaseEvent(event)
     972
     973
     974 -        def _addDiscs(self, discIdListNode, release):
     975                  for node in _getChildElements(discIdListNode):
     976                          if _matches(node, 'disc') and node.hasAttribute('id'):
     977                                  d = self._factory.newDisc()
     978                                  d.setId(node.getAttribute('id'))
     979                                  d.setSectors(_getIntAttr(node, 'sectors', 0))
     980                                  release.addDisc(d)
     981
     982
     983 -        def _addArtistAliases(self, aliasListNode, artist):
     984                  for node in _getChildElements(aliasListNode):
     985                          if _matches(node, 'alias'):
     986                                  alias = self._factory.newArtistAlias()
     987                                  self._initializeAlias(alias, node)
     988                                  artist.addAlias(alias)
     989
     990
     991 -        def _addLabelAliases(self, aliasListNode, label):
     992                  for node in _getChildElements(aliasListNode):
     993                          if _matches(node, 'alias'):
     994                                  alias = self._factory.newLabelAlias()
     995                                  self._initializeAlias(alias, node)
     996                                  label.addAlias(alias)
     997
     998
     999 -        def _initializeAlias(self, alias, node):
    1000                  alias.setValue(_getText(node))
    1001                  alias.setType(_getUriAttr(node, 'type'))
    1002                  alias.setScript(_getAttr(node, 'script',
    1003                          '^[A-Z][a-z]{3}$'))
    1004
    1005
    1006 -        def _createTrack(self, trackNode):
    1007                  track = self._factory.newTrack()
    1008                  track.setId(_getIdAttr(trackNode, 'id', 'track'))
    1009
    1010                  for node in _getChildElements(trackNode):
    1011                          if _matches(node, 'title'):
    1012                                  track.setTitle(_getText(node))
    1013                          elif _matches(node, 'artist'):
    1014                                  track.setArtist(self._createArtist(node))
    1015                          elif _matches(node, 'duration'):
    1016                                  track.setDuration(_getPositiveIntText(node))
    1017                          elif _matches(node, 'release-list'):
    1018                                  self._addReleasesToList(node, track.getReleases())
    1019                          elif _matches(node, 'puid-list'):
    1020                                  self._addPuids(node, track)
    1021                          elif _matches(node, 'isrc-list'):
    1022                                  self._addISRCs(node, track)
    1023                          elif _matches(node, 'relation-list'):
    1024                                  self._addRelationsToEntity(node, track)
    1025                          elif _matches(node, 'tag-list'):
    1026                                  self._addTagsToEntity(node, track)
    1027                          elif _matches(node, 'rating'):
    1028                                  self._addRatingToEntity(node, track)
    1029
    1030                  return track
    1031
    1032          # MusicBrainz extension
    1033 -        def _createUser(self, userNode):
    1034                  user = self._factory.newUser()
    1035                  for t in _getUriListAttr(userNode, 'type', NS_EXT_1):
    1036                          user.addType(t)
    1037
    1038                  for node in _getChildElements(userNode):
    1039                          if _matches(node, 'name'):
    1040                                  user.setName(_getText(node))
    1041                          elif _matches(node, 'nag', NS_EXT_1):
    1042                                  user.setShowNag(_getBooleanAttr(node, 'show'))
    1043
    1044                  return user
    1045
    1046 -        def _createRating(self, ratingNode):
    1047                  rating = self._factory.newRating()
    1048                  rating.value = _getText(ratingNode)
    1049                  rating.count = _getIntAttr(ratingNode, 'votes-count')
    1050                  return rating
    1051
    1052 -        def _createTag(self, tagNode):
    1053                  tag = self._factory.newTag()
    1054                  tag.value = _getText(tagNode)
    1055                  tag.count = _getIntAttr(tagNode, 'count')
    1056                  return tag
    1057
    1058
    1059 -        def _addPuids(self, puidListNode, track):
    1060                  for node in _getChildElements(puidListNode):
    1061                          if _matches(node, 'puid') and node.hasAttribute('id'):
    1062                                  track.addPuid(node.getAttribute('id'))
    1063
    1064 -        def _addISRCs(self, isrcListNode, track):
    1065                  for node in _getChildElements(isrcListNode):
    1066                          if _matches(node, 'isrc') and node.hasAttribute('id'):
    1067                                  track.addISRC(node.getAttribute('id'))
    1068
    1069 -        def _addRelationsToEntity(self, relationListNode, entity):
    1070                  targetType = _getUriAttr(relationListNode, 'target-type', NS_REL_1)
    1071
    1072                  if targetType is None:
    1073                          return
    1074
    1075                  for node in _getChildElements(relationListNode):
    1076                          if _matches(node, 'relation'):
    1077                                  rel = self._createRelation(node, targetType)
    1078                                  if rel is not None:
    1079                                          entity.addRelation(rel)
    1080
    1081
    1082 -        def _createRelation(self, relationNode, targetType):
    1083                  relation = self._factory.newRelation()
    1084
    1085                  relation.setType(_getUriAttr(relationNode, 'type', NS_REL_1))
    1086                  relation.setTargetType(targetType)
    1087                  resType = _getResourceType(targetType)
    1088                  relation.setTargetId(_getIdAttr(relationNode, 'target', resType))
    1089
    1090                  if relation.getType() is None \
    1091                                  or relation.getTargetType() is None \
    1092                                  or relation.getTargetId() is None:
    1093                          return None
    1094
    1095                  relation.setDirection(_getDirectionAttr(relationNode, 'direction'))
    1096                  relation.setBeginDate(_getDateAttr(relationNode, 'begin'))
    1097                  relation.setEndDate(_getDateAttr(relationNode, 'end'))
    1098
    1099                  for a in _getUriListAttr(relationNode, 'attributes', NS_REL_1):
    1100                          relation.addAttribute(a)
    1101
    1102                  target = None
    1103                  children = _getChildElements(relationNode)
    1104                  if len(children) > 0:
    1105                          node = children[0]
    1106                          if _matches(node, 'artist'):
    1107                                  target = self._createArtist(node)
    1108                          elif _matches(node, 'release'):
    1109                                  target = self._createRelease(node)
    1110                          elif _matches(node, 'track'):
    1111                                  target = self._createTrack(node)
    1112
    1113                  relation.setTarget(target)
    1114
    1115                  return relation
    1116
    1117
    1118  #
    1119  # XML output
    1120  #
    1121
    1122 -class _XmlWriter(object):
    1123 -        def __init__(self, outStream, indentAmount='  ', newline="\n"):
    1124                  self._out = outStream
    1125                  self._indentAmount = indentAmount
    1126                  self._stack = [ ]
    1127                  self._newline = newline
    1128
    1129 -        def prolog(self, encoding='UTF-8', version='1.0'):
    1130                  pi = '<?xml version="%s" encoding="%s"?>' % (version, encoding)
    1131                  self._out.write(pi + self._newline)
    1132
    1133 -        def start(self, name, attrs={ }):
    1134                  indent = self._getIndention()
    1135                  self._stack.append(name)
    1136                  self._out.write(indent + self._makeTag(name, attrs) + self._newline)
    1137
    1138 -        def end(self):
    1139                  name = self._stack.pop()
    1140                  indent = self._getIndention()
    1141                  self._out.write('%s</%s>\n' % (indent, name))
    1142
    1143 -        def elem(self, name, value, attrs={ }):
    1144                  # delete attributes with an unset value
    1145                  for (k, v) in attrs.items():
    1146                          if v is None or v == '':
    1147                                  del attrs[k]
    1148
    1149                  if value is None or value == '':
    1150                          if len(attrs) == 0:
    1151                                  return
    1152                          self._out.write(self._getIndention())
    1153                          self._out.write(self._makeTag(name, attrs, True) + '\n')
    1154                  else:
    1155                          escValue = saxutils.escape(value or '')
    1156                          self._out.write(self._getIndention())
    1157                          self._out.write(self._makeTag(name, attrs))
    1158                          self._out.write(escValue)
    1159                          self._out.write('</%s>\n' % name)
    1160
    1161 -        def _getIndention(self):
    1162                  return self._indentAmount * len(self._stack)
    1163
    1164 -        def _makeTag(self, name, attrs={ }, close=False):
    1165                  ret = '<' + name
    1166
    1167                  for (k, v) in attrs.iteritems():
    1168                          if v is not None:
    1169                                  v = saxutils.quoteattr(str(v))
    1170                                  ret += ' %s=%s' % (k, v)
    1171
    1172                  if close:
    1173                          return ret + '/>'
    1174                  else:
    1175                          return ret + '>'
    1176
    1177
    1178
    1179 -class MbXmlWriter(object):
    1180          """Write XML in the Music Metadata XML format."""
    1181
    1182 -        def __init__(self, indentAmount='  ', newline="\n"):
    1183                  """Constructor.
    1184
    1185                  @param indentAmount: the amount of whitespace to use per level
    1186                  """
    1187                  self._indentAmount = indentAmount
    1188                  self._newline = newline
    1189
    1190
    1191 -        def write(self, outStream, metadata):
    1192                  """Writes the XML representation of a Metadata object to a file.
    1193
    1194                  @param outStream: an open file-like object
    1195                  @param metadata: a L{Metadata} object
    1196                  """
    1197                  xml = _XmlWriter(outStream, self._indentAmount, self._newline)
    1198
    1199                  xml.prolog()
    1200                  xml.start('metadata', {
    1201                          'xmlns': NS_MMD_1,
    1202                          'xmlns:ext': NS_EXT_1,
    1203                  })
    1204
    1205                  self._writeArtist(xml, metadata.getArtist())
    1206                  self._writeRelease(xml, metadata.getRelease())
    1207                  self._writeReleaseGroup(xml, metadata.getReleaseGroup())
    1208                  self._writeTrack(xml, metadata.getTrack())
    1209                  self._writeLabel(xml, metadata.getLabel())
    1210
    1211                  if len(metadata.getArtistResults()) > 0:
    1212                          xml.start('artist-list', {
    1213                                  'offset': metadata.artistResultsOffset,
    1214                                  'count': metadata.artistResultsCount,
    1215                          })
    1216                          for result in metadata.getArtistResults():
    1217                                  self._writeArtist(xml, result.getArtist(),
    1218                                          result.getScore())
    1219                          xml.end()
    1220
    1221                  if len(metadata.getReleaseResults()) > 0:
    1222                          xml.start('release-list', {
    1223                                  'offset': metadata.releaseResultsOffset,
    1224                                  'count': metadata.releaseResultsCount,
    1225                          })
    1226                          for result in metadata.getReleaseResults():
    1227                                  self._writeRelease(xml, result.getRelease(),
    1228                                          result.getScore())
    1229                          xml.end()
    1230
    1231                  if len(metadata.getReleaseGroupResults()) > 0:
    1232                          xml.start('release-group-list', {
    1233                                  'offset': metadata.releaseGroupResultsOffset,
    1234                                  'count': metadata.releaseGroupResultsCount
    1235                          })
    1236                          for result in metadata.getReleaseGroupResults():
    1237                                  self._writeReleaseGroup(xml, result.getReleaseGroup(),
    1238                                          result.getScore())
    1239                          xml.end()
    1240
    1241                  if len(metadata.getTrackResults()) > 0:
    1242                          xml.start('track-list', {
    1243                                  'offset': metadata.trackResultsOffset,
    1244                                  'count': metadata.trackResultsCount,
    1245                          })
    1246                          for result in metadata.getTrackResults():
    1247                                  self._writeTrack(xml, result.getTrack(),
    1248                                          result.getScore())
    1249                          xml.end()
    1250
    1251                  if len(metadata.getLabelResults()) > 0:
    1252                          xml.start('label-list', {
    1253                                  'offset': metadata.labelResultsOffset,
    1254                                  'count': metadata.labelResultsCount,
    1255                          })
    1256                          for result in metadata.getLabelResults():
    1257                                  self._writeLabel(xml, result.getLabel(),
    1258                                          result.getScore())
    1259                          xml.end()
    1260
    1261                  xml.end()
    1262
    1263
    1264 -        def _writeArtist(self, xml, artist, score=None):
    1265                  if artist is None:
    1266                          return
    1267
    1268                  xml.start('artist', {
    1269                          'id': mbutils.extractUuid(artist.getId()),
    1270                          'type': mbutils.extractFragment(artist.getType()),
    1271                          'ext:score': score,
    1272                  })
    1273
    1274                  xml.elem('name', artist.getName())
    1275                  xml.elem('sort-name', artist.getSortName())
    1276                  xml.elem('disambiguation', artist.getDisambiguation())
    1277                  xml.elem('life-span', None, {
    1278                          'begin': artist.getBeginDate(),
    1279                          'end': artist.getEndDate(),
    1280                  })
    1281
    1282                  if len(artist.getAliases()) > 0:
    1283                          xml.start('alias-list')
    1284                          for alias in artist.getAliases():
    1285                                  xml.elem('alias', alias.getValue(), {
    1286                                          'type': alias.getType(),
    1287                                          'script': alias.getScript(),
    1288                                  })
    1289                          xml.end()
    1290
    1291                  if len(artist.getReleases()) > 0:
    1292                          xml.start('release-list')
    1293                          for release in artist.getReleases():
    1294                                  self._writeRelease(xml, release)
    1295                          xml.end()
    1296
    1297                  if len(artist.getReleaseGroups()) > 0:
    1298                          xml.start('release-group-list')
    1299                          for releaseGroup in artist.getReleaseGroups():
    1300                                  self._writeReleaseGroup(xml, releaseGroup)
    1301                          xml.end()
    1302
    1303                  self._writeRelationList(xml, artist)
    1304                  # TODO: extensions
    1305
    1306                  xml.end()
    1307
    1308
    1309 -        def _writeRelease(self, xml, release, score=None):
    1310                  if release is None:
    1311                          return
    1312
    1313                  types = [mbutils.extractFragment(t) for t in release.getTypes()]
    1314                  typesStr = None
    1315                  if len(types) > 0:
    1316                          typesStr = ' '.join(types)
    1317
    1318                  xml.start('release', {
    1319                          'id': mbutils.extractUuid(release.getId()),
    1320                          'type': typesStr,
    1321                          'ext:score': score,
    1322                  })
    1323
    1324                  xml.elem('title', release.getTitle())
    1325                  xml.elem('text-representation', None, {
    1326                          'language': release.getTextLanguage(),
    1327                          'script': release.getTextScript()
    1328                  })
    1329                  xml.elem('asin', release.getAsin())
    1330
    1331                  self._writeArtist(xml, release.getArtist())
    1332                  self._writeReleaseGroup(xml, release.getReleaseGroup())
    1333
    1334                  if len(release.getReleaseEvents()) > 0:
    1335                          xml.start('release-event-list')
    1336                          for event in release.getReleaseEvents():
    1337                                  self._writeReleaseEvent(xml, event)
    1338                          xml.end()
    1339
    1340                  if len(release.getDiscs()) > 0:
    1341                          xml.start('disc-list')
    1342                          for disc in release.getDiscs():
    1343                                  xml.elem('disc', None, { 'id': disc.getId() })
    1344                          xml.end()
    1345
    1346                  if len(release.getTracks()) > 0:
    1347                          # TODO: count attribute
    1348                          xml.start('track-list', {
    1349                                  'offset': release.getTracksOffset()
    1350                          })
    1351                          for track in release.getTracks():
    1352                                  self._writeTrack(xml, track)
    1353                          xml.end()
    1354
    1355                  self._writeRelationList(xml, release)
    1356                  # TODO: extensions
    1357
    1358                  xml.end()
    1359
    1360 -        def _writeReleaseGroup(self, xml, rg, score = None):
    1361                  if rg is None:
    1362                          return
    1363
    1364                  xml.start('release-group', {
    1365                          'id': mbutils.extractUuid(rg.getId()),
    1366                          'type': mbutils.extractFragment(rg.getType()),
    1367                          'ext:score': score,
    1368                  })
    1369
    1370                  xml.elem('title', rg.getTitle())
    1371                  self._writeArtist(xml, rg.getArtist())
    1372
    1373                  if len(rg.getReleases()) > 0:
    1374                          xml.start('release-list')
    1375                          for rel in rg.getReleases():
    1376                                  self._writeRelease(xml, rel)
    1377                          xml.end()
    1378
    1379                  xml.end()
    1380
    1381 -        def _writeReleaseEvent(self, xml, event):
    1382                  xml.start('event', {
    1383                          'country': event.getCountry(),
    1384                          'date': event.getDate(),
    1385                          'catalog-number': event.getCatalogNumber(),
    1386                          'barcode': event.getBarcode(),
    1387                          'format': event.getFormat()
    1388                  })
    1389
    1390                  self._writeLabel(xml, event.getLabel())
    1391
    1392                  xml.end()
    1393
    1394
    1395 -        def _writeTrack(self, xml, track, score=None):
    1396                  if track is None:
    1397                          return
    1398
    1399                  xml.start('track', {
    1400                          'id': mbutils.extractUuid(track.getId()),
    1401                          'ext:score': score,
    1402                  })
    1403
    1404                  xml.elem('title', track.getTitle())
    1405                  xml.elem('duration', str(track.getDuration()))
    1406                  self._writeArtist(xml, track.getArtist())
    1407
    1408                  if len(track.getReleases()) > 0:
    1409                          # TODO: offset + count
    1410                          xml.start('release-list')
    1411                          for release in track.getReleases():
    1412                                  self._writeRelease(xml, release)
    1413                          xml.end()
    1414
    1415                  if len(track.getPuids()) > 0:
    1416                          xml.start('puid-list')
    1417                          for puid in track.getPuids():
    1418                                  xml.elem('puid', None, { 'id': puid })
    1419                          xml.end()
    1420
    1421                  self._writeRelationList(xml, track)
    1422                  # TODO: extensions
    1423
    1424                  xml.end()
    1425
    1426
    1427 -        def _writeLabel(self, xml, label, score=None):
    1428                  if label is None:
    1429                          return
    1430
    1431                  xml.start('label', {
    1432                          'id': mbutils.extractUuid(label.getId()),
    1433                          'type': mbutils.extractFragment(label.getType()),
    1434                          'ext:score': score,
    1435                  })
    1436
    1437                  xml.elem('name', label.getName())
    1438                  xml.elem('sort-name', label.getSortName())
    1439                  xml.elem('disambiguation', label.getDisambiguation())
    1440                  xml.elem('life-span', None, {
    1441                          'begin': label.getBeginDate(),
    1442                          'end': label.getEndDate(),
    1443                  })
    1444
    1445                  if len(label.getAliases()) > 0:
    1446                          xml.start('alias-list')
    1447                          for alias in label.getAliases():
    1448                                  xml.elem('alias', alias.getValue(), {
    1449                                          'type': alias.getType(),
    1450                                          'script': alias.getScript(),
    1451                                  })
    1452                          xml.end()
    1453
    1454                  # TODO: releases, artists
    1455
    1456                  self._writeRelationList(xml, label)
    1457                  # TODO: extensions
    1458
    1459                  xml.end()
    1460
    1461
    1462 -        def _writeRelationList(self, xml, entity):
    1463                  for tt in entity.getRelationTargetTypes():
    1464                          xml.start('relation-list', {
    1465                                  'target-type': mbutils.extractFragment(tt),
    1466                          })
    1467                          for rel in entity.getRelations(targetType=tt):
    1468                                  self._writeRelation(xml, rel, tt)
    1469                          xml.end()
    1470
    1471
    1472 -        def _writeRelation(self, xml, rel, targetType):
    1473                  relAttrs = ' '.join([mbutils.extractFragment(a)
    1474                                  for a in rel.getAttributes()])
    1475
    1476                  if relAttrs == '':
    1477                          relAttrs = None
    1478
    1479                  attrs = {
    1480                          'type': mbutils.extractFragment(rel.getType()),
    1481                          'target': rel.getTargetId(),
    1482                          'direction': rel.getDirection(),
    1483                          'begin': rel.getBeginDate(),
    1484                          'end': rel.getBeginDate(),
    1485                          'attributes': relAttrs,
    1486                  }
    1487
    1488                  if rel.getTarget() is None:
    1489                          xml.elem('relation', None, attrs)
    1490                  else:
    1491                          xml.start('relation', attrs)
    1492                          if targetType == NS_REL_1 + 'Artist':
    1493                                  self._writeArtist(xml, rel.getTarget())
    1494                          elif targetType == NS_REL_1 + 'Release':
    1495                                  self._writeRelease(xml, rel.getTarget())
    1496                          elif targetType == NS_REL_1 + 'Track':
    1497                                  self._writeTrack(xml, rel.getTarget())
    1498                          xml.end()
    1499
    1500
    1501  #
    1502  # DOM Utilities
    1503  #
    1504
    1505 -def _matches(node, name, namespace=NS_MMD_1):
    1506          """Checks if an xml.dom.Node and a given name and namespace match."""
    1507
    1508          if node.localName == name and node.namespaceURI == namespace:
    1509                  return True
    1510          else:
    1511                  return False
    1512
    1513
    1514 -def _getChildElements(parentNode):
    1515          """Returns all direct child elements of the given xml.dom.Node."""
    1516
    1517          children = [ ]
    1518          for node in parentNode.childNodes:
    1519                  if node.nodeType == node.ELEMENT_NODE:
    1520                          children.append(node)
    1521
    1522          return children
    1523
    1524
    1525 -def _getText(element, regex=None, default=None):
    1526          """Returns the text content of the given xml.dom.Element.
    1527
    1528          This function simply fetches all contained text nodes, so the element
    1529          should not contain child elements.
    1530          """
    1531          res = ''
    1532          for node in element.childNodes:
    1533                  if node.nodeType == node.TEXT_NODE:
    1534                          res += node.data
    1535
    1536          if regex is None or re.match(regex, res):
    1537                  return res
    1538          else:
    1539                  return default
    1540
    1541
    1542 -def _getPositiveIntText(element):
    1543          """Returns the text content of the given xml.dom.Element as an int."""
    1544
    1545          res = _getText(element)
    1546
    1547          if res is None:
    1548                  return None
    1549
    1550          try:
    1551                  return int(res)
    1552          except ValueError:
    1553                  return None
    1554
    1555
    1556 -def _getAttr(element, attrName, regex=None, default=None, ns=None):
    1557          """Returns an attribute of the given element.
    1558
    1559          If there is no attribute with that name or the attribute doesn't
    1560          match the regular expression, default is returned.
    1561          """
    1562          if element.hasAttributeNS(ns, attrName):
    1563                  content = element.getAttributeNS(ns, attrName)
    1564
    1565                  if regex is None or re.match(regex, content):
    1566                          return content
    1567                  else:
    1568                          return default
    1569          else:
    1570                  return default
    1571
    1572
    1573 -def _getDateAttr(element, attrName):
    1574          """Gets an incomplete date from an attribute."""
    1575          return _getAttr(element, attrName, '^\d+(-\d\d)?(-\d\d)?$')
    1576
    1577
    1578 -def _getIdAttr(element, attrName, typeName):
    1579          """Gets an ID from an attribute and turns it into an absolute URI."""
    1580          value = _getAttr(element, attrName)
    1581
    1582          return _makeAbsoluteUri('http://musicbrainz.org/' + typeName + '/', value)
    1583
    1584
    1585
    1586 -def _getIntAttr(element, attrName, min=0, max=None, ns=None):
    1587          """Gets an int from an attribute, or None."""
    1588          try:
    1589                  val = int(_getAttr(element, attrName, ns=ns))
    1590
    1591                  if max is None:
    1592                          max = val
    1593
    1594                  if min <= val <= max:
    1595                          return val
    1596                  else:
    1597                          return None
    1598          except ValueError:
    1599                  return None # raised if conversion to int fails
    1600          except TypeError:
    1601                  return None # raised if no such attribute exists
    1602
    1603
    1604 -def _getUriListAttr(element, attrName, prefix=NS_MMD_1):
    1605          """Gets a list of URIs from an attribute."""
    1606          if not element.hasAttribute(attrName):
    1607                  return [ ]
    1608
    1609          f = lambda x: x != ''
    1610          uris = filter(f, re.split('\s+', element.getAttribute(attrName)))
    1611
    1612          m = lambda x: _makeAbsoluteUri(prefix, x)
    1613          uris = map(m, uris)
    1614
    1615          return uris
    1616
    1617
    1618 -def _getUriAttr(element, attrName, prefix=NS_MMD_1):
    1619          """Gets a URI from an attribute.
    1620
    1621          This also works for space-separated URI lists. In this case, the
    1622          first URI is returned.
    1623          """
    1624          uris = _getUriListAttr(element, attrName, prefix)
    1625          if len(uris) > 0:
    1626                  return uris[0]
    1627          else:
    1628                  return None
    1629
    1630
    1631 -def _getBooleanAttr(element, attrName):
    1632          """Gets a boolean value from an attribute."""
    1633          value = _getAttr(element, attrName)
    1634          if value == 'true':
    1635                  return True
    1636          elif value == 'false':
    1637                  return False
    1638          else:
    1639                  return None
    1640
    1641
    1642 -def _getDirectionAttr(element, attrName):
    1643          """Gets the Relation reading direction from an attribute."""
    1644          regex = '^\s*(' + '|'.join((
    1645                                  model.Relation.DIR_FORWARD,
    1646                                  model.Relation.DIR_BACKWARD)) + ')\s*$'
    1647          return _getAttr(element, 'direction', regex, model.Relation.DIR_NONE)
    1648
    1649
    1650 -def _makeAbsoluteUri(prefix, uriStr):
    1651          """Creates an absolute URI adding prefix, if necessary."""
    1652          if uriStr is None:
    1653                  return None
    1654
    1655          (scheme, netloc, path, params, query, frag) = urlparse.urlparse(uriStr)
    1656
    1657          if scheme == '' and netloc == '':
    1658                  return prefix + uriStr
    1659          else:
    1660                  return uriStr
    1661
    1662
    1663 -def _getResourceType(uri):
    1664          """Gets the resource type from a URI.
    1665
    1666          The resource type is the basename of the URI's path.
    1667          """
    1668          m = re.match('^' + NS_REL_1 + '(.*)$', uri)
    1669
    1670          if m:
    1671                  return m.group(1).lower()
    1672          else:
    1673                  return None
    1674
    1675  # EOF
    1676