← 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:
  OpenLP Core (openlp-core)

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

- 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/45619
Your team OpenLP Core is requested to review the proposed merge of lp:~googol-hush/openlp/OpenLyrics into lp:openlp.
=== 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-08 20:54:57 +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-08 20:54:57 +0000
@@ -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-08 20:54:57 +0000
@@ -175,6 +175,7 @@
         return None
     return filter(lambda item: item[1] == choice[0], encodings)[0][0]
 
-from xml import LyricsXML, SongXMLBuilder, SongXMLParser, OpenLyricsParser
+from xml import OpenLyricsBuilder, OpenLyricsParser, SongXMLBuilder, \
+    SongXMLParser
 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-08 20:54:57 +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-05 16:50:28 +0000
+++ openlp/plugins/songs/lib/mediaitem.py	2011-01-08 20:54:57 +0000
@@ -35,7 +35,8 @@
     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 OpenLyricsBuilder, OpenLyricsParser, \
+    SongXMLParser
 from openlp.plugins.songs.lib.db import Author, Song
 from openlp.core.lib.searchedit import SearchEdit
 
@@ -58,7 +59,8 @@
         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.openLyricsParser = OpenLyricsParser(self.parent.manager)
+        self.openLyricsBuilder = OpenLyricsBuilder(self.parent.manager)
         self.singleServiceItem = False
         self.song_maintenance_form = SongMaintenanceForm(
             self.parent.manager, self)
@@ -312,15 +314,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]
@@ -394,9 +395,9 @@
         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.xml_version = self.openLyrics.song_to_xml(song)
+        service_item.data_string = {u'title': song.search_title,
+            u'authors': author_list}
+        service_item.xml_version = self.openLyricsBuilder.song_to_xml(song)
         return True
 
     def serviceLoad(self, item):
@@ -407,7 +408,7 @@
         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() ,
+                item.data_string[u'title'].split(u'@')[0].lower(),
                 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
@@ -416,7 +417,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:
@@ -439,11 +439,11 @@
                         break
             if add_song:
                 if self.addSongFromService:
-                    editId = self.openLyrics.xml_to_song(item.xml_version)
+                    editId = self.openLyricsParser.xml_to_song(item.xml_version)
             # 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-08 20:54:57 +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 OpenLyricsParser
+
+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.openLyricsParser = OpenLyricsParser(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.openLyricsParser.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/xml.py'
--- openlp/plugins/songs/lib/xml.py	2010-12-26 11:04:47 +0000
+++ openlp/plugins/songs/lib/xml.py	2011-01-08 20:54:57 +0000
@@ -24,9 +24,9 @@
 # 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">
@@ -36,14 +36,38 @@
             </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__)
 
@@ -80,8 +104,8 @@
         ``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)
 
@@ -142,117 +166,115 @@
         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 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': []}]
+#        for counter, verse in enumerate(verses):
+#            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 OpenLyricsBuilder(object):
+    """
+    This class represents the converter for song to OpenLyrics XML.
     """
     def __init__(self, manager):
         self.manager = manager
 
-    def song_to_xml(self, song):
+    def song_to_xml(self, song, pretty_print=False):
         """
-        Convert the song to OpenLyrics Format
+        Convert the song to OpenLyrics Format.
         """
         song_xml_parser = SongXMLParser(song.lyrics)
         verse_list = song_xml_parser.get_verses()
@@ -263,16 +285,33 @@
         self._add_text_to_element(u'title', titles, song.title)
         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)
+        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,118 +321,364 @@
             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 _add_text_to_element(self, tag, parent, text=None, label=None):
+        if label:
+            element = etree.Element(tag, name=unicode(label))
+        else:
+            element = etree.Element(tag)
+        if text:
+            element.text = unicode(text)
+        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 _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)
+
+
+class OpenLyricsParser(object):
+    """
+    This class represents the converter for OpenLyrics XML to 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 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
+        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':
+        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
+
+    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''
-        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:
+
+    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 = SongXMLBuilder()
+        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
-        if theme_name:
-            song.theme_name = theme_name
-        else:
-            song.theme_name = u''
-        # Process Titles
+
+    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 = unicode(title.text)
+                song.title = self._text(title)
                 song.search_title = unicode(song.title)
                 song.alternate_title = u''
             else:
-                song.alternate_title = unicode(title.text)
+                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()
-        # 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
+
+    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 author in properties.authors.author:
-                self._process_author(author.text, song)
-        except:
-            # No Author in XML so ignore
+            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
-        self.manager.save_object(song)
-        return song.id
-
-    def _add_text_to_element(self, tag, parent, text=None, label=None):
-        if label:
-            element = etree.Element(tag, name=unicode(label))
-        else:
-            element = etree.Element(tag)
-        if text:
-            element.text = unicode(text)
-        parent.append(element)
-        return element
-
-    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