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/45620 - 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/45620 Your team OpenLP Core is requested to review the proposed merge of lp:~googol-hush/openlp/OpenLyrics into lp:openlp.
=== modified file 'openlp/core/ui/__init__.py' --- openlp/core/ui/__init__.py 2011-01-05 09:37:11 +0000 +++ openlp/core/ui/__init__.py 2011-01-08 21:02:32 +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-08 21:02:32 +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-08 21:02:32 +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/songimportform.py' --- openlp/plugins/songs/forms/songimportform.py 2010-12-29 16:35:10 +0000 +++ openlp/plugins/songs/forms/songimportform.py 2011-01-08 21:02:32 +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 21:02:32 +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-08 21:02:32 +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 21:02:32 +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 21:02:32 +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 21:02:32 +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 21:02:32 +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
_______________________________________________ Mailing list: https://launchpad.net/~openlp-core Post to : openlp-core@lists.launchpad.net Unsubscribe : https://launchpad.net/~openlp-core More help : https://help.launchpad.net/ListHelp