← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~googol-hush/openlp/OpenLyrics into lp:openlp

 

Andreas Preikschat has proposed merging lp:~googol-hush/openlp/OpenLyrics into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)

For more details, see:
https://code.launchpad.net/~googol-hush/openlp/OpenLyrics/+merge/45646

- added OpenLyrics importer
- continued to implement OpenLyrics features
- split class
- fix wrong use of "theme"
- and other thinks

Importing songs with multiple languages has not been considered (yet). (One song with all languages is imported.)

Cheers
-- 
https://code.launchpad.net/~googol-hush/openlp/OpenLyrics/+merge/45646
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/ui/__init__.py'
--- openlp/core/ui/__init__.py	2011-01-05 16:57:49 +0000
+++ openlp/core/ui/__init__.py	2011-01-09 19:37:54 +0000
@@ -35,11 +35,11 @@
     ``Blank``
         This mode is used to hide all output, specifically by covering the
         display with a black screen.
-        
+
     ``Theme``
         This mode is used to hide all output, but covers the display with the
         current theme background, as opposed to black.
-        
+
     ``Desktop``
         This mode hides all output by minimising the display, leaving the user's
         desktop showing.

=== modified file 'openlp/plugins/bibles/forms/bibleimportwizard.py'
--- openlp/plugins/bibles/forms/bibleimportwizard.py	2010-12-27 22:57:35 +0000
+++ openlp/plugins/bibles/forms/bibleimportwizard.py	2011-01-09 19:37:54 +0000
@@ -234,7 +234,7 @@
             QtGui.QSizePolicy.Minimum)
         self.openlp1Layout.setItem(1, QtGui.QFormLayout.LabelRole,
             self.openlp1Spacer)
-        self.selectStack.addWidget(self.openlp1Widget) 
+        self.selectStack.addWidget(self.openlp1Widget)
         self.selectPageLayout.addLayout(self.selectStack)
         bibleImportWizard.addPage(self.selectPage)
         # License Page

=== modified file 'openlp/plugins/songs/forms/authorsdialog.py'
--- openlp/plugins/songs/forms/authorsdialog.py	2011-01-04 21:06:50 +0000
+++ openlp/plugins/songs/forms/authorsdialog.py	2011-01-09 19:37:54 +0000
@@ -54,7 +54,7 @@
         self.displayEdit.setObjectName(u'displayEdit')
         self.displayLabel.setBuddy(self.displayEdit)
         self.authorLayout.addRow(self.displayLabel, self.displayEdit)
-        self.dialogLayout.addLayout(self.authorLayout) 
+        self.dialogLayout.addLayout(self.authorLayout)
         self.buttonBox = QtGui.QDialogButtonBox(authorsDialog)
         self.buttonBox.setStandardButtons(
             QtGui.QDialogButtonBox.Save | QtGui.QDialogButtonBox.Cancel)

=== modified file 'openlp/plugins/songs/forms/editsongform.py'
--- openlp/plugins/songs/forms/editsongform.py	2011-01-05 10:48:47 +0000
+++ openlp/plugins/songs/forms/editsongform.py	2011-01-09 19:37:54 +0000
@@ -31,7 +31,7 @@
 
 from openlp.core.lib import Receiver, translate
 from openlp.plugins.songs.forms import EditVerseForm
-from openlp.plugins.songs.lib import SongXMLBuilder, SongXMLParser, VerseType
+from openlp.plugins.songs.lib import SongXML, VerseType
 from openlp.plugins.songs.lib.db import Book, Song, Author, Topic
 from editsongdialog import Ui_EditSongDialog
 
@@ -263,8 +263,8 @@
         if isinstance(self.song.lyrics, buffer):
             self.song.lyrics = unicode(self.song.lyrics)
         if self.song.lyrics.startswith(u'<?xml version='):
-            songXML = SongXMLParser(self.song.lyrics)
-            verseList = songXML.get_verses()
+            songXML = SongXML()
+            verseList = songXML.get_verses(self.song.lyrics)
             for count, verse in enumerate(verseList):
                 self.verseListWidget.setRowCount(
                     self.verseListWidget.rowCount() + 1)
@@ -731,7 +731,7 @@
     def processLyrics(self):
         log.debug(u'processLyrics')
         try:
-            sxml = SongXMLBuilder()
+            sxml = SongXML()
             text = u''
             multiple = []
             for i in range(0, self.verseListWidget.rowCount()):

=== modified file 'openlp/plugins/songs/forms/songimportform.py'
--- openlp/plugins/songs/forms/songimportform.py	2010-12-29 16:35:10 +0000
+++ openlp/plugins/songs/forms/songimportform.py	2011-01-09 19:37:54 +0000
@@ -73,12 +73,12 @@
         QtCore.QObject.connect(self.openLP1BrowseButton,
             QtCore.SIGNAL(u'clicked()'),
             self.onOpenLP1BrowseButtonClicked)
-        #QtCore.QObject.connect(self.openLyricsAddButton,
-        #    QtCore.SIGNAL(u'clicked()'),
-        #    self.onOpenLyricsAddButtonClicked)
-        #QtCore.QObject.connect(self.openLyricsRemoveButton,
-        #    QtCore.SIGNAL(u'clicked()'),
-        #    self.onOpenLyricsRemoveButtonClicked)
+        QtCore.QObject.connect(self.openLyricsAddButton,
+            QtCore.SIGNAL(u'clicked()'),
+            self.onOpenLyricsAddButtonClicked)
+        QtCore.QObject.connect(self.openLyricsRemoveButton,
+            QtCore.SIGNAL(u'clicked()'),
+            self.onOpenLyricsRemoveButtonClicked)
         QtCore.QObject.connect(self.openSongAddButton,
             QtCore.SIGNAL(u'clicked()'),
             self.onOpenSongAddButtonClicked)
@@ -167,16 +167,15 @@
                     self.openLP1BrowseButton.setFocus()
                     return False
             elif source_format == SongFormat.OpenLyrics:
-#                if self.openLyricsFileListWidget.count() == 0:
-#                    QtGui.QMessageBox.critical(self,
-#                        translate('SongsPlugin.ImportWizardForm',
-#                        'No OpenLyrics Files Selected'),
-#                        translate('SongsPlugin.ImportWizardForm',
-#                        'You need to add at least one OpenLyrics '
-#                        'song file to import from.'))
-#                    self.openLyricsAddButton.setFocus()
-#                    return False
-                return False
+                if self.openLyricsFileListWidget.count() == 0:
+                    QtGui.QMessageBox.critical(self,
+                        translate('SongsPlugin.ImportWizardForm',
+                        'No OpenLyrics Files Selected'),
+                        translate('SongsPlugin.ImportWizardForm',
+                        'You need to add at least one OpenLyrics '
+                        'song file to import from.'))
+                    self.openLyricsAddButton.setFocus()
+                    return False
             elif source_format == SongFormat.OpenSong:
                 if self.openSongFileListWidget.count() == 0:
                     QtGui.QMessageBox.critical(self,
@@ -337,15 +336,15 @@
             'openlp.org v1.x Databases')
         )
 
-    #def onOpenLyricsAddButtonClicked(self):
-    #    self.getFiles(
-    #        translate('SongsPlugin.ImportWizardForm',
-    #        'Select OpenLyrics Files'),
-    #        self.openLyricsFileListWidget
-    #    )
+    def onOpenLyricsAddButtonClicked(self):
+        self.getFiles(
+            translate('SongsPlugin.ImportWizardForm',
+            'Select OpenLyrics Files'),
+            self.openLyricsFileListWidget
+        )
 
-    #def onOpenLyricsRemoveButtonClicked(self):
-    #    self.removeSelectedItems(self.openLyricsFileListWidget)
+    def onOpenLyricsRemoveButtonClicked(self):
+        self.removeSelectedItems(self.openLyricsFileListWidget)
 
     def onOpenSongAddButtonClicked(self):
         self.getFiles(
@@ -435,7 +434,7 @@
         self.formatComboBox.setCurrentIndex(0)
         self.openLP2FilenameEdit.setText(u'')
         self.openLP1FilenameEdit.setText(u'')
-        #self.openLyricsFileListWidget.clear()
+        self.openLyricsFileListWidget.clear()
         self.openSongFileListWidget.clear()
         self.wordsOfWorshipFileListWidget.clear()
         self.ccliFileListWidget.clear()

=== modified file 'openlp/plugins/songs/forms/songimportwizard.py'
--- openlp/plugins/songs/forms/songimportwizard.py	2010-12-27 18:23:46 +0000
+++ openlp/plugins/songs/forms/songimportwizard.py	2011-01-09 19:37:54 +0000
@@ -39,7 +39,7 @@
             QtGui.QWizard.IndependentPages |
             QtGui.QWizard.NoBackButtonOnStartPage |
             QtGui.QWizard.NoBackButtonOnLastPage)
-        # Welcome Page 
+        # Welcome Page
         self.welcomePage = QtGui.QWizardPage()
         self.welcomePage.setPixmap(QtGui.QWizard.WatermarkPixmap,
             QtGui.QPixmap(u':/wizards/wizard_importsong.bmp'))
@@ -81,9 +81,6 @@
         self.addSingleFileSelectItem(u'openLP1', None, True)
         # OpenLyrics
         self.addMultiFileSelectItem(u'openLyrics', u'OpenLyrics', True)
-        # set OpenLyrics to disabled by default
-        self.openLyricsDisabledWidget.setVisible(True)
-        self.openLyricsImportWidget.setVisible(False)
         # Open Song
         self.addMultiFileSelectItem(u'openSong', u'OpenSong')
         # Words of Worship
@@ -177,10 +174,10 @@
             'importer has been disabled due to a missing Python module. If '
             'you want to use this importer, you will need to install the '
             '"python-sqlite" module.'))
-        #self.openLyricsAddButton.setText(
-        #    translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
-        #self.openLyricsRemoveButton.setText(
-        #    translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
+        self.openLyricsAddButton.setText(
+            translate('SongsPlugin.ImportWizardForm', 'Add Files...'))
+        self.openLyricsRemoveButton.setText(
+            translate('SongsPlugin.ImportWizardForm', 'Remove File(s)'))
         self.openLyricsDisabledLabel.setText(
             translate('SongsPlugin.ImportWizardForm', 'The OpenLyrics '
             'importer has not yet been developed, but as you can see, we are '

=== modified file 'openlp/plugins/songs/lib/__init__.py'
--- openlp/plugins/songs/lib/__init__.py	2011-01-04 10:13:41 +0000
+++ openlp/plugins/songs/lib/__init__.py	2011-01-09 19:37:54 +0000
@@ -175,6 +175,6 @@
         return None
     return filter(lambda item: item[1] == choice[0], encodings)[0][0]
 
-from xml import LyricsXML, SongXMLBuilder, SongXMLParser, OpenLyricsParser
+from xml import OpenLyrics, SongXML
 from songstab import SongsTab
 from mediaitem import SongMediaItem

=== modified file 'openlp/plugins/songs/lib/importer.py'
--- openlp/plugins/songs/lib/importer.py	2010-12-26 11:04:47 +0000
+++ openlp/plugins/songs/lib/importer.py	2011-01-09 19:37:54 +0000
@@ -26,6 +26,7 @@
 
 from opensongimport import OpenSongImport
 from olpimport import OpenLPSongImport
+from openlyricsimport import OpenLyricsImport
 from wowimport import WowImport
 from cclifileimport import CCLIFileImport
 from ewimport import EasyWorshipSongImport
@@ -77,8 +78,10 @@
         """
         if format == SongFormat.OpenLP2:
             return OpenLPSongImport
-        if format == SongFormat.OpenLP1:
+        elif format == SongFormat.OpenLP1:
             return OpenLP1SongImport
+        elif format == SongFormat.OpenLyrics:
+            return OpenLyricsImport
         elif format == SongFormat.OpenSong:
             return OpenSongImport
         elif format == SongFormat.SongsOfFellowship:
@@ -93,7 +96,6 @@
             return EasyWorshipSongImport
         elif format == SongFormat.SongBeamer:
             return SongBeamerImport
-#        else:
         return None
 
     @staticmethod

=== modified file 'openlp/plugins/songs/lib/mediaitem.py'
--- openlp/plugins/songs/lib/mediaitem.py	2011-01-07 18:58:06 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2011-01-09 19:37:54 +0000
@@ -35,7 +35,7 @@
     ItemCapabilities, translate, check_item_selected
 from openlp.plugins.songs.forms import EditSongForm, SongMaintenanceForm, \
     SongImportForm
-from openlp.plugins.songs.lib import SongXMLParser, OpenLyricsParser
+from openlp.plugins.songs.lib import OpenLyrics, SongXML
 from openlp.plugins.songs.lib.db import Author, Song
 from openlp.core.lib.searchedit import SearchEdit
 
@@ -58,7 +58,7 @@
         self.ListViewWithDnD_class = SongListView
         MediaManagerItem.__init__(self, parent, self, icon)
         self.edit_song_form = EditSongForm(self, self.parent.manager)
-        self.openLyrics = OpenLyricsParser(self.parent.manager)
+        self.openLyrics = OpenLyrics(self.parent.manager)
         self.singleServiceItem = False
         self.song_maintenance_form = SongMaintenanceForm(
             self.parent.manager, self)
@@ -322,15 +322,14 @@
             translate('SongsPlugin.MediaItem',
             'You must select an item to delete.')):
             items = self.listView.selectedIndexes()
-            ans = QtGui.QMessageBox.question(self,
+            if QtGui.QMessageBox.question(self,
                 translate('SongsPlugin.MediaItem', 'Delete Song(s)?'),
                 translate('SongsPlugin.MediaItem',
                 'Are you sure you want to delete the %n selected song(s)?', '',
                 QtCore.QCoreApplication.CodecForTr, len(items)),
-                QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok|
-                     QtGui.QMessageBox.Cancel),
-                QtGui.QMessageBox.Ok)
-            if ans == QtGui.QMessageBox.Cancel:
+                QtGui.QMessageBox.StandardButtons(QtGui.QMessageBox.Ok |
+                QtGui.QMessageBox.Cancel),
+                QtGui.QMessageBox.Ok) == QtGui.QMessageBox.Cancel:
                 return
             for item in items:
                 item_id = (item.data(QtCore.Qt.UserRole)).toInt()[0]
@@ -362,8 +361,7 @@
         service_item.theme = song.theme_name
         service_item.edit_id = item_id
         if song.lyrics.startswith(u'<?xml version='):
-            songXML = SongXMLParser(song.lyrics)
-            verseList = songXML.get_verses()
+            verseList = SongXML().get_verses(song.lyrics)
             # no verse list or only 1 space (in error)
             if not song.verse_order or not song.verse_order.strip():
                 for verse in verseList:
@@ -404,8 +402,8 @@
         service_item.audit = [
             song.title, author_audit, song.copyright, unicode(song.ccli_number)
         ]
-        service_item.data_string = {u'title':song.search_title,
-            u'authors':author_list}
+        service_item.data_string = {u'title': song.search_title,
+            u'authors': author_list}
         service_item.xml_version = self.openLyrics.song_to_xml(song)
         return True
 
@@ -416,8 +414,8 @@
         log.debug(u'serviceLoad')
         if item.data_string:
             search_results = self.parent.manager.get_all_objects(Song,
-                Song.search_title ==
-                    item.data_string[u'title'].split(u'@')[0].lower() ,
+                Song.search_title == re.compile(r'\W+', re.UNICODE).sub(u' ',
+                item.data_string[u'title'].split(u'@')[0].lower()).strip(),
                 Song.search_title.asc())
             author_list = item.data_string[u'authors'].split(u', ')
             # The service item always has an author (at least it has u'' as
@@ -426,7 +424,6 @@
             if u'' in author_list:
                 author_list.remove(u'')
             editId = 0
-            uuid = item._uuid
             add_song = True
             if search_results:
                 for song in search_results:
@@ -453,7 +450,7 @@
             # Update service with correct song id.
             if editId != 0:
                 Receiver.send_message(u'service_item_update',
-                    u'%s:%s' %(editId, uuid))
+                    u'%s:%s' % (editId, item._uuid))
 
     def collateSongTitles(self, song_1, song_2):
         """

=== added file 'openlp/plugins/songs/lib/openlyricsimport.py'
--- openlp/plugins/songs/lib/openlyricsimport.py	1970-01-01 00:00:00 +0000
+++ openlp/plugins/songs/lib/openlyricsimport.py	2011-01-09 19:37:54 +0000
@@ -0,0 +1,77 @@
+# -*- coding: utf-8 -*-
+# vim: autoindent shiftwidth=4 expandtab textwidth=80 tabstop=4 softtabstop=4
+
+###############################################################################
+# OpenLP - Open Source Lyrics Projection                                      #
+# --------------------------------------------------------------------------- #
+# Copyright (c) 2008-2011 Raoul Snyman                                        #
+# Portions copyright (c) 2008-2010 Tim Bentley, Jonathan Corwin, Michael      #
+# Gorven, Scott Guerrieri, Meinert Jordan, Andreas Preikschat, Christian      #
+# Richter, Philip Ridout, Maikel Stuivenberg, Martin Thompson, Jon Tibble,    #
+# Carsten Tinggaard, Frode Woldsund                                           #
+# --------------------------------------------------------------------------- #
+# This program is free software; you can redistribute it and/or modify it     #
+# under the terms of the GNU General Public License as published by the Free  #
+# Software Foundation; version 2 of the License.                              #
+#                                                                             #
+# This program is distributed in the hope that it will be useful, but WITHOUT #
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or       #
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for    #
+# more details.                                                               #
+#                                                                             #
+# You should have received a copy of the GNU General Public License along     #
+# with this program; if not, write to the Free Software Foundation, Inc., 59  #
+# Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
+###############################################################################
+"""
+The :mod:`openlyricsimport` module provides the functionality for importing
+songs which are saved as OpenLyrics files.
+"""
+
+import logging
+import os
+
+from lxml import etree
+
+from openlp.core.lib import translate
+from openlp.plugins.songs.lib.songimport import SongImport
+from openlp.plugins.songs.lib import OpenLyrics
+
+log = logging.getLogger(__name__)
+
+class OpenLyricsImport(SongImport):
+    """
+    This provides the Openlyrics import.
+    """
+    def __init__(self, master_manager, **kwargs):
+        """
+        Initialise the import.
+        """
+        log.debug(u'initialise OpenLyricsImport')
+        SongImport.__init__(self, master_manager)
+        self.master_manager = master_manager
+        self.openLyrics = OpenLyrics(master_manager)
+        if kwargs.has_key(u'filename'):
+            self.import_source = kwargs[u'filename']
+        if kwargs.has_key(u'filenames'):
+            self.import_source = kwargs[u'filenames']
+
+    def do_import(self):
+        """
+        Imports the songs.
+        """
+        self.import_wizard.importProgressBar.setMaximum(len(self.import_source))
+        for file_path in self.import_source:
+            if self.stop_import_flag:
+                return False
+            self.import_wizard.incrementProgressBar(unicode(translate(
+                'SongsPlugin.OpenLyricsImport', 'Importing %s...')) %
+                os.path.basename(file_path))
+            parser = etree.XMLParser(remove_blank_text=True)
+            file = etree.parse(file_path, parser)
+            xml = unicode(etree.tostring(file))
+            if self.openLyrics.xml_to_song(xml) == 0:
+                log.debug(u'File could not be imported: %s' % file_path)
+                # Importing this song failed! For now we stop import.
+                return False
+        return True

=== modified file 'openlp/plugins/songs/lib/songimport.py'
--- openlp/plugins/songs/lib/songimport.py	2010-12-26 11:04:47 +0000
+++ openlp/plugins/songs/lib/songimport.py	2011-01-09 19:37:54 +0000
@@ -31,7 +31,7 @@
 from openlp.core.lib import Receiver, translate
 from openlp.plugins.songs.lib import VerseType
 from openlp.plugins.songs.lib.db import Song, Author, Topic, Book, MediaFile
-from openlp.plugins.songs.lib.xml import SongXMLBuilder
+from openlp.plugins.songs.lib.xml import SongXML
 
 log = logging.getLogger(__name__)
 
@@ -270,7 +270,7 @@
         song.song_number = self.song_number
         song.search_lyrics = u''
         verses_changed_to_other = {}
-        sxml = SongXMLBuilder()
+        sxml = SongXML()
         other_count = 1
         for (versetag, versetext) in self.verses:
             if versetag[0] == u'C':

=== modified file 'openlp/plugins/songs/lib/xml.py'
--- openlp/plugins/songs/lib/xml.py	2010-12-26 11:04:47 +0000
+++ openlp/plugins/songs/lib/xml.py	2011-01-09 19:37:54 +0000
@@ -24,55 +24,73 @@
 # Temple Place, Suite 330, Boston, MA 02111-1307 USA                          #
 ###############################################################################
 """
-The :mod:`xml` module provides the XML functionality for songs
+The :mod:`xml` module provides the XML functionality.
 
-The basic XML is of the format::
+The basic XML for storing the lyrics in the song database is of the format::
 
     <?xml version="1.0" encoding="UTF-8"?>
     <song version="1.0">
-        <lyrics language="en">
+        <lyrics>
             <verse type="chorus" label="1">
                 <![CDATA[ ... ]]>
             </verse>
         </lyrics>
     </song>
+
+
+The XML of `OpenLyrics <http://openlyrics.info/>`_  songs is of the format::
+
+    <song xmlns="http://openlyrics.info/namespace/2009/song";
+        version="0.7"
+        createdIn="OpenLP 1.9.0"
+        modifiedIn="ChangingSong 0.0.1"
+        modifiedDate="2010-01-28T13:15:30+01:00">
+    <properties>
+        <titles>
+            <title>Amazing Grace</title>
+        </titles>
+    </properties>
+        <lyrics>
+            <verse name="v1">
+                <lines>
+                    <line>Amazing grace how sweet the sound</line>
+                </lines>
+            </verse>
+        </lyrics>
+    </song>
 """
 
 import logging
 import re
 
 from lxml import etree, objectify
+
+from openlp.core.lib import translate
 from openlp.plugins.songs.lib import VerseType
-from openlp.plugins.songs.lib.db import Author, Song
+from openlp.plugins.songs.lib.db import Author, Book, Song, Topic
 
 log = logging.getLogger(__name__)
 
-class SongXMLBuilder(object):
-    """
-    This class builds the XML used to describe songs.
-    """
-    log.info(u'SongXMLBuilder Loaded')
-
-    def __init__(self, song_language=None):
-        """
-        Set up the song builder.
-
-        ``song_language``
-            The language used in this song
-        """
-        lang = u'en'
-        if song_language:
-            lang = song_language
+class SongXML(object):
+    """
+    This class builds and parses the XML used to describe songs.
+    """
+    log.info(u'SongXML Loaded')
+
+    def __init__(self):
+        """
+        Set up the default variables.
+        """
         self.song_xml = objectify.fromstring(u'<song version="1.0" />')
-        self.lyrics = etree.SubElement(self.song_xml, u'lyrics', language=lang)
+        self.lyrics = etree.SubElement(self.song_xml, u'lyrics')
 
     def add_verse_to_lyrics(self, type, number, content):
         """
-        Add a verse to the ``<lyrics>`` tag.
+        Add a verse to the *<lyrics>* tag.
 
         ``type``
-            A string denoting the type of verse. Possible values are "Chorus",
-            "Verse", "Bridge", and "Custom".
+            A string denoting the type of verse. Possible values are "V",
+            "C", "B", "P", "I", "E" and "O".
 
         ``number``
             An integer denoting the number of the item, for example: verse 1.
@@ -80,18 +98,11 @@
         ``content``
             The actual text of the verse to be stored.
         """
-        verse = etree.Element(u'verse', type = unicode(type),
-            label = unicode(number))
+        verse = etree.Element(u'verse', type=unicode(type),
+            label=unicode(number))
         verse.text = etree.CDATA(content)
         self.lyrics.append(verse)
 
-    def dump_xml(self):
-        """
-        Debugging aid to dump XML so that we can see what we have.
-        """
-        return etree.tostring(self.song_xml, encoding=u'UTF-8',
-            xml_declaration=True, pretty_print=True)
-
     def extract_xml(self):
         """
         Extract our newly created XML song.
@@ -99,16 +110,10 @@
         return etree.tostring(self.song_xml, encoding=u'UTF-8',
             xml_declaration=True)
 
-
-class SongXMLParser(object):
-    """
-    A class to read in and parse a song's XML.
-    """
-    log.info(u'SongXMLParser Loaded')
-
-    def __init__(self, xml):
+    def get_verses(self, xml):
         """
-        Set up our song XML parser.
+        Iterates through the verses in the XML and returns a list of verses
+        and their attributes.
 
         ``xml``
             The XML of the song to be parsed.
@@ -120,12 +125,6 @@
             self.song_xml = objectify.fromstring(xml)
         except etree.XMLSyntaxError:
             log.exception(u'Invalid xml %s', xml)
-
-    def get_verses(self):
-        """
-        Iterates through the verses in the XML and returns a list of verses
-        and their attributes.
-        """
         xml_iter = self.song_xml.getiterator()
         verse_list = []
         for element in xml_iter:
@@ -142,137 +141,109 @@
         return etree.dump(self.song_xml)
 
 
-class LyricsXML(object):
-    """
-    This class represents the XML in the ``lyrics`` field of a song.
-    """
-    def __init__(self, song=None):
-        if song:
-            if song.lyrics.startswith(u'<?xml'):
-                self.parse(song.lyrics)
-            else:
-                self.extract(song.lyrics)
-        else:
-            self.languages = []
-
-    def parse(self, xml):
-        """
-        Parse XML from the ``lyrics`` field in the database, and set the list
-        of verses from it.
-
-        ``xml``
-            The XML to parse.
-        """
-        try:
-            self.languages = []
-            song = objectify.fromstring(xml)
-            for lyrics in song.lyrics:
-                language = {
-                    u'language': lyrics.attrib[u'language'],
-                    u'verses': []
-                }
-                for verse in lyrics.verse:
-                    language[u'verses'].append({
-                        u'type': verse.attrib[u'type'],
-                        u'label': verse.attrib[u'label'],
-                        u'text': unicode(verse.text)
-                    })
-                self.lyrics.append(language)
-            return True
-        except etree.XMLSyntaxError:
-            return False
-
-    def extract(self, text):
-        """
-        If the ``lyrics`` field in the database is not XML, this method is
-        called and used to construct the verse structure similar to the output
-        of the ``parse`` function.
-
-        ``text``
-            The text to pull verses out of.
-        """
-        text = text.replace('\r\n', '\n')
-        verses = text.split('\n\n')
-        self.languages = [{u'language': u'en', u'verses': []}]
-        counter = 0
-        for verse in verses:
-            counter = counter + 1
-            self.languages[0][u'verses'].append({
-                u'type': u'verse',
-                u'label': unicode(counter),
-                u'text': verse
-            })
-        return True
-
-    def add_verse(self, type, label, text):
-        """
-        Add a verse to the list of verses.
-
-        ``type``
-            The type of list, one of "verse", "chorus", "bridge", "pre-chorus",
-            "intro", "outtro".
-
-        ``label``
-            The number associated with this verse, like 1 or 2.
-
-        ``text``
-            The text of the verse.
-        """
-        self.verses.append({
-            u'type': type,
-            u'label': label,
-            u'text': text
-        })
-
-    def export(self):
-        """
-        Build up the XML for the verse structure.
-        """
-        lyrics_output = u''
-        for language in self.languages:
-            verse_output = u''
-            for verse in language[u'verses']:
-                verse_output = verse_output + \
-                    u'<verse type="%s" label="%s"><![CDATA[%s]]></verse>' % \
-                    (verse[u'type'], verse[u'label'], verse[u'text'])
-            lyrics_output = lyrics_output + \
-                u'<lyrics language="%s">%s</lyrics>' % \
-                (language[u'language'], verse_output)
-        song_output = u'<?xml version="1.0" encoding="UTF-8"?>' + \
-            u'<song version="1.0">%s</song>' % lyrics_output
-        return song_output
-
-
-class OpenLyricsParser(object):
-    """
-    This class represents the converter for Song to/from OpenLyrics XML.
+class OpenLyrics(object):
+    """
+    This class represents the converter for OpenLyrics XML to/from a song.
+
+    As OpenLyrics has a rich set of different features, we cannot support them
+    all. The following features are supported by the :class:`OpenLyricsParser`::
+
+    *<authors>*
+        OpenLP does not support the attribute *type* and *lang*.
+
+    *<chord>*
+        This property is not supported.
+
+    *<comments>*
+        The *<comments>* property is fully supported. But comments in lyrics
+        are not supported.
+
+    *<copyright>*
+        This property is fully supported.
+
+    *<customVersion>*
+        This property is not supported.
+
+    *<key>*
+        This property is not supported.
+
+    *<keywords>*
+        This property is not supported.
+
+    *<lines>*
+        The attribute *part* is not supported.
+
+    *<publisher>*
+        This property is not supported.
+
+    *<songbooks>*
+        As OpenLP does only support one songbook, we cannot consider more than
+        one songbook.
+
+    *<tempo>*
+        This property is not supported.
+
+    *<themes>*
+        Topics, as they are called in OpenLP, are fully supported, whereby only
+        the topic text (e. g. Grace) is considered, but neither the *id* nor
+        *lang*.
+
+    *<transposition>*
+        This property is not supported.
+
+    *<variant>*
+        This property is not supported.
+
+    *<verse name="v1a" lang="he" translit="en">*
+        The attribute *translit* and *lang* are not supported.
+
+    *<verseOrder>*
+        OpenLP supports this property.
     """
     def __init__(self, manager):
         self.manager = manager
 
-    def song_to_xml(self, song):
-        """
-        Convert the song to OpenLyrics Format
-        """
-        song_xml_parser = SongXMLParser(song.lyrics)
-        verse_list = song_xml_parser.get_verses()
+    def song_to_xml(self, song, pretty_print=False):
+        """
+        Convert the song to OpenLyrics Format.
+        """
+        sxml = SongXML()
+        verse_list = sxml.get_verses(song.lyrics)
         song_xml = objectify.fromstring(
             u'<song version="0.7" createdIn="OpenLP 2.0"/>')
         properties = etree.SubElement(song_xml, u'properties')
         titles = etree.SubElement(properties, u'titles')
-        self._add_text_to_element(u'title', titles, song.title)
+        self._add_text_to_element(u'title', titles, song.title.strip())
         if song.alternate_title:
-            self._add_text_to_element(u'title', titles, song.alternate_title)
-        if song.theme_name:
-            themes = etree.SubElement(properties, u'themes')
-            self._add_text_to_element(u'theme', themes, song.theme_name)
-        self._add_text_to_element(u'copyright', properties, song.copyright)
-        self._add_text_to_element(u'verseOrder', properties, song.verse_order)
+            self._add_text_to_element(
+                u'title', titles, song.alternate_title.strip())
+        if song.comments:
+            comments = etree.SubElement(properties, u'comments')
+            self._add_text_to_element(u'comment', comments, song.comments)
+        if song.copyright:
+            self._add_text_to_element(u'copyright', properties, song.copyright)
+        if song.verse_order:
+            self._add_text_to_element(
+                u'verseOrder', properties, song.verse_order)
         if song.ccli_number:
             self._add_text_to_element(u'ccliNo', properties, song.ccli_number)
-        authors = etree.SubElement(properties, u'authors')
-        for author in song.authors:
-            self._add_text_to_element(u'author', authors, author.display_name)
+        if song.authors:
+            authors = etree.SubElement(properties, u'authors')
+            for author in song.authors:
+                self._add_text_to_element(
+                    u'author', authors, author.display_name)
+        book = self.manager.get_object_filtered(
+            Book, Book.id == song.song_book_id)
+        if book is not None:
+            book = book.name
+            songbooks = etree.SubElement(properties, u'songbooks')
+            element = self._add_text_to_element(
+                u'songbook', songbooks, None, book)
+            element.set(u'entry', song.song_number)
+        if song.topics:
+            themes = etree.SubElement(properties, u'themes')
+            for topic in song.topics:
+                self._add_text_to_element(u'theme', themes, topic.name)
         lyrics = etree.SubElement(song_xml, u'lyrics')
         for verse in verse_list:
             verse_tag = u'%s%s' % (
@@ -282,78 +253,36 @@
             element = self._add_text_to_element(u'lines', element)
             for line in unicode(verse[1]).split(u'\n'):
                 self._add_text_to_element(u'line', element, line)
-        return self._extract_xml(song_xml)
+        return self._extract_xml(song_xml, pretty_print)
 
     def xml_to_song(self, xml):
         """
-        Create a Song from OpenLyrics format xml
+        Create and save a song from OpenLyrics format xml to the database. Since
+        we also export XML from external sources (e. g. OpenLyrics import), we
+        cannot ensure, that it completely conforms to the OpenLyrics standard.
+
+        ``xml``
+            The XML to parse (unicode).
         """
-        # No xml get out of here
+        # No xml get out of here.
         if not xml:
             return 0
         song = Song()
         if xml[:5] == u'<?xml':
             xml = xml[38:]
+        # Remove chords from xml.
+        xml = re.compile(u'<chord name=".*?"/>').sub(u'', xml)
         song_xml = objectify.fromstring(xml)
         properties = song_xml.properties
-        song.copyright = unicode(properties.copyright.text)
-        if song.copyright == u'None':
-            song.copyright = u''
-        song.verse_order = unicode(properties.verseOrder.text)
-        if song.verse_order == u'None':
-            song.verse_order = u''
-        song.topics = []
-        song.book = None
-        theme_name = None
-        try:
-            song.ccli_number = unicode(properties.ccliNo.text)
-        except:
-            song.ccli_number = u''
-        try:
-            theme_name = unicode(properties.themes.theme)
-        except:
-            pass
-        if theme_name:
-            song.theme_name = theme_name
-        else:
-            song.theme_name = u''
-        # Process Titles
-        for title in properties.titles.title:
-            if not song.title:
-                song.title = unicode(title.text)
-                song.search_title = unicode(song.title)
-                song.alternate_title = u''
-            else:
-                song.alternate_title = unicode(title.text)
-                song.search_title += u'@' + song.alternate_title
-        song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'',
-            unicode(song.search_title)).lower()
-        # Process Lyrics
-        sxml = SongXMLBuilder()
-        search_text = u''
-        for lyrics in song_xml.lyrics:
-            for verse in song_xml.lyrics.verse:
-                text = u''
-                for line in verse.lines.line:
-                    line = unicode(line)
-                    if not text:
-                        text = line
-                    else:
-                        text += u'\n' + line
-                type = VerseType.expand_string(verse.attrib[u'name'][0])
-                sxml.add_verse_to_lyrics(type, verse.attrib[u'name'][1], text)
-                search_text = search_text + text
-        song.search_lyrics = search_text.lower()
-        song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
-        song.comments = u''
-        song.song_number = u''
-        # Process Authors
-        try:
-            for author in properties.authors.author:
-                self._process_author(author.text, song)
-        except:
-            # No Author in XML so ignore
-            pass
+        self._process_copyright(properties, song)
+        self._process_cclinumber(properties, song)
+        self._process_titles(properties, song)
+        # The verse order is processed with the lyrics!
+        self._process_lyrics(properties, song_xml.lyrics, song)
+        self._process_comments(properties, song)
+        self._process_authors(properties, song)
+        self._process_songbooks(properties, song)
+        self._process_topics(properties, song)
         self.manager.save_object(song)
         return song.id
 
@@ -367,33 +296,258 @@
         parent.append(element)
         return element
 
+    def _extract_xml(self, xml, pretty_print):
+        """
+        Extract our newly created XML song.
+        """
+        return etree.tostring(xml, encoding=u'UTF-8',
+            xml_declaration=True, pretty_print=pretty_print)
+
+    def _get(self, element, attribute):
+        """
+        This returns the element's attribute as unicode string.
+
+        ``element``
+            The element.
+
+        ``attribute``
+            The element's attribute (unicode).
+        """
+        if element.get(attribute) is not None:
+            return unicode(element.get(attribute))
+        return u''
+
+    def _text(self, element):
+        """
+        This returns the text of an element as unicode string.
+
+        ``element``
+            The element.
+        """
+        if element.text is not None:
+            return unicode(element.text)
+        return u''
+
+    def _process_authors(self, properties, song):
+        """
+        Adds the authors specified in the XML to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        authors = []
+        try:
+            for author in properties.authors.author:
+                display_name = self._text(author)
+                if display_name:
+                    authors.append(display_name)
+        except AttributeError:
+            pass
+        if not authors:
+            # Add "Author unknown" (can be translated).
+            authors.append((unicode(translate('SongsPlugin.XML',
+                'Author unknown'))))
+        for display_name in authors:
+            author = self.manager.get_object_filtered(Author,
+                Author.display_name == display_name)
+            if author is None:
+                # We need to create a new author, as the author does not exist.
+                author = Author.populate(display_name=display_name,
+                    last_name=display_name.split(u' ')[-1],
+                    first_name=u' '.join(display_name.split(u' ')[:-1]))
+            self.manager.save_object(author)
+            song.authors.append(author)
+
+    def _process_cclinumber(self, properties, song):
+        """
+        Adds the CCLI number to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        try:
+            song.ccli_number = self._text(properties.ccliNo)
+        except AttributeError:
+            song.ccli_number = u''
+
+    def _process_comments(self, properties, song):
+        """
+        Joins the comments specified in the XML and add it to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        try:
+            comments_list = []
+            for comment in properties.comments.comment:
+                commenttext = self._text(comment)
+                if commenttext:
+                    comments_list.append(commenttext)
+            song.comments = u'\n'.join(comments_list)
+        except AttributeError:
+            song.comments = u''
+
+    def _process_copyright(self, properties, song):
+        """
+        Adds the copyright to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        try:
+            song.copyright = self._text(properties.copyright)
+        except AttributeError:
+            song.copyright = u''
+
+    def _process_lyrics(self, properties, lyrics, song):
+        """
+        Processes the verses and search_lyrics for the song.
+
+        ``properties``
+            The properties object (lxml.objectify.ObjectifiedElement).
+
+        ``lyrics``
+            The lyrics object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        sxml = SongXML()
+        search_text = u''
+        temp_verse_order = []
+        for verse in lyrics.verse:
+            text = u''
+            for lines in verse.lines:
+                if text:
+                    text += u'\n'
+                text += u'\n'.join([unicode(line) for line in lines.line])
+            verse_name = self._get(verse, u'name')
+            verse_type = unicode(VerseType.expand_string(verse_name[0]))[0]
+            verse_number = re.compile(u'[a-zA-Z]*').sub(u'', verse_name)
+            verse_part = re.compile(u'[0-9]*').sub(u'', verse_name[1:])
+            # OpenLyrics allows e. g. "c", but we need "c1".
+            if not verse_number:
+                verse_number = u'1'
+            temp_verse_order.append((verse_type, verse_number, verse_part))
+            sxml.add_verse_to_lyrics(verse_type, verse_number, text)
+            search_text = search_text + text
+        song.search_lyrics = search_text.lower()
+        song.lyrics = unicode(sxml.extract_xml(), u'utf-8')
+        # Process verse order
+        try:
+            song.verse_order = self._text(properties.verseOrder)
+        except AttributeError:
+            # We have to process the temp_verse_order, as the verseOrder
+            # property is not present.
+            previous_type = u''
+            previous_number = u''
+            previous_part = u''
+            verse_order = []
+            # Currently we do not support different "parts"!
+            for name in temp_verse_order:
+                if name[0] == previous_type:
+                    if name[1] != previous_number:
+                        verse_order.append(u''.join((name[0], name[1])))
+                else:
+                    verse_order.append(u''.join((name[0], name[1])))
+                previous_type = name[0]
+                previous_number = name[1]
+                previous_part = name[2]
+            song.verse_order = u' '.join(verse_order)
+
+    def _process_songbooks(self, properties, song):
+        """
+        Adds the song book and song number specified in the XML to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        song.song_book_id = 0
+        song.song_number = u''
+        try:
+            for songbook in properties.songbooks.songbook:
+                bookname = self._get(songbook, u'name')
+                if bookname:
+                    book = self.manager.get_object_filtered(Book,
+                        Book.name == bookname)
+                    if book is None:
+                        # We need to create a book, because it does not exist.
+                        book = Book.populate(name=bookname, publisher=u'')
+                        self.manager.save_object(book)
+                    song.song_book_id = book.id
+                    try:
+                        if self._get(songbook, u'entry'):
+                            song.song_number = self._get(songbook, u'entry')
+                    except AttributeError:
+                        pass
+                    # We does only support one song book, so take the first one.
+                    break
+        except AttributeError:
+            pass
+
+    def _process_titles(self, properties, song):
+        """
+        Processes the titles specified in the song's XML.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        for title in properties.titles.title:
+            if not song.title:
+                song.title = self._text(title)
+                song.search_title = unicode(song.title)
+                song.alternate_title = u''
+            else:
+                song.alternate_title = self._text(title)
+                song.search_title += u'@' + song.alternate_title
+        song.search_title = re.sub(r'[\'"`,;:(){}?]+', u'',
+            unicode(song.search_title)).lower()
+
+    def _process_topics(self, properties, song):
+        """
+        Adds the topics to the song.
+
+        ``properties``
+            The property object (lxml.objectify.ObjectifiedElement).
+
+        ``song``
+            The song object.
+        """
+        try:
+            for topictext in properties.themes.theme:
+                topictext = self._text(topictext)
+                if topictext:
+                    topic = self.manager.get_object_filtered(Topic,
+                        Topic.name == topictext)
+                    if topic is None:
+                        # We need to create a topic, because it does not exist.
+                        topic = Topic.populate(name=topictext)
+                        self.manager.save_object(topic)
+                    song.topics.append(topic)
+        except AttributeError:
+            pass
+
     def _dump_xml(self, xml):
         """
         Debugging aid to dump XML so that we can see what we have.
         """
         return etree.tostring(xml, encoding=u'UTF-8',
             xml_declaration=True, pretty_print=True)
-
-    def _extract_xml(self, xml):
-        """
-        Extract our newly created XML song.
-        """
-        return etree.tostring(xml, encoding=u'UTF-8',
-            xml_declaration=True)
-
-    def _process_author(self, name, song):
-        """
-        Find or create an Author from display_name.
-        """
-        name = unicode(name)
-        author = self.manager.get_object_filtered(Author,
-            Author.display_name == name)
-        if author:
-            # should only be one! so take the first
-            song.authors.append(author)
-        else:
-            # Need a new author
-            new_author = Author.populate(first_name=name.rsplit(u' ', 1)[0],
-                        last_name=name.rsplit(u' ', 1)[1], display_name=name)
-            self.manager.save_object(new_author)
-            song.authors.append(new_author)
\ No newline at end of file

=== modified file 'openlp/plugins/songs/songsplugin.py'
--- openlp/plugins/songs/songsplugin.py	2010-12-31 02:17:41 +0000
+++ openlp/plugins/songs/songsplugin.py	2011-01-09 19:37:54 +0000
@@ -31,7 +31,7 @@
 
 from openlp.core.lib import Plugin, StringContent, build_icon, translate
 from openlp.core.lib.db import Manager
-from openlp.plugins.songs.lib import SongMediaItem, SongsTab, SongXMLParser
+from openlp.plugins.songs.lib import SongMediaItem, SongsTab, SongXML
 from openlp.plugins.songs.lib.db import init_schema, Song
 from openlp.plugins.songs.lib.importer import SongFormat
 
@@ -153,7 +153,7 @@
             song.search_title = self.whitespace.sub(u' ', song.title.lower() + \
                 u' ' + song.alternate_title.lower())
             lyrics = u''
-            verses = SongXMLParser(song.lyrics).get_verses()
+            verses = SongXML().get_verses(song.lyrics)
             for verse in verses:
                 lyrics = lyrics + self.whitespace.sub(u' ', verse[1]) + u' '
             song.search_lyrics = lyrics.lower()


Follow ups